+NULL =
EXTRA_DIST = misc/config.js.in
CLEANFILES = misc/config.js
ui/altTab.js \
ui/appDisplay.js \
ui/appFavorites.js \
- ui/automountManager.js \
- ui/autorunManager.js \
ui/boxpointer.js \
ui/calendar.js \
ui/checkBox.js \
ui/dateMenu.js \
ui/dnd.js \
ui/endSessionDialog.js \
- ui/environment.js \
ui/extensionSystem.js \
ui/extensionDownloader.js \
+ ui/environment.js \
ui/flashspot.js \
ui/ibusCandidatePopup.js\
ui/grabHelper.js \
ui/iconGrid.js \
ui/keyboard.js \
- ui/keyringPrompt.js \
ui/layout.js \
ui/lightbox.js \
ui/lookingGlass.js \
ui/main.js \
ui/messageTray.js \
ui/modalDialog.js \
- ui/networkAgent.js \
ui/sessionMode.js \
ui/shellEntry.js \
ui/shellMountOperation.js \
ui/panelMenu.js \
ui/placeDisplay.js \
ui/pointerWatcher.js \
- ui/polkitAuthenticationAgent.js \
ui/popupMenu.js \
ui/remoteSearch.js \
ui/runDialog.js \
ui/status/power.js \
ui/status/volume.js \
ui/status/bluetooth.js \
- ui/telepathyClient.js \
ui/tweener.js \
ui/unlockDialog.js \
ui/userMenu.js \
ui/workspaceThumbnail.js \
ui/workspacesView.js \
ui/workspaceSwitcherPopup.js \
- ui/xdndHandler.js
+ ui/xdndHandler.js \
+ ui/components/__init__.js \
+ ui/components/autorunManager.js \
+ ui/components/automountManager.js \
+ ui/components/networkAgent.js \
+ ui/components/polkitAgent.js \
+ ui/components/recorder.js \
+ ui/components/telepathyClient.js \
+ ui/components/keyring.js \
+ $(NULL)
+++ /dev/null
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-
-const Lang = imports.lang;
-const Mainloop = imports.mainloop;
-const GLib = imports.gi.GLib;
-const Gio = imports.gi.Gio;
-const Params = imports.misc.params;
-const Shell = imports.gi.Shell;
-
-const GnomeSession = imports.misc.gnomeSession;
-const LoginManager = imports.misc.loginManager;
-const Main = imports.ui.main;
-const ShellMountOperation = imports.ui.shellMountOperation;
-
-const GNOME_SESSION_AUTOMOUNT_INHIBIT = 16;
-
-// GSettings keys
-const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
-const SETTING_ENABLE_AUTOMOUNT = 'automount';
-
-const AUTORUN_EXPIRE_TIMEOUT_SECS = 10;
-
-const AutomountManager = new Lang.Class({
- Name: 'AutomountManager',
-
- _init: function() {
- this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
- this._volumeQueue = [];
- this._session = new GnomeSession.SessionManager();
- this._session.connectSignal('InhibitorAdded',
- Lang.bind(this, this._InhibitorsChanged));
- this._session.connectSignal('InhibitorRemoved',
- Lang.bind(this, this._InhibitorsChanged));
- this._inhibited = false;
-
- this._loginManager = LoginManager.getLoginManager();
-
- Main.screenShield.connect('lock-status-changed', Lang.bind(this, this._lockStatusChanged));
-
- this._volumeMonitor = Gio.VolumeMonitor.get();
-
- this._volumeMonitor.connect('volume-added',
- Lang.bind(this,
- this._onVolumeAdded));
- this._volumeMonitor.connect('volume-removed',
- Lang.bind(this,
- this._onVolumeRemoved));
- this._volumeMonitor.connect('drive-connected',
- Lang.bind(this,
- this._onDriveConnected));
- this._volumeMonitor.connect('drive-disconnected',
- Lang.bind(this,
- this._onDriveDisconnected));
- this._volumeMonitor.connect('drive-eject-button',
- Lang.bind(this,
- this._onDriveEjectButton));
-
- Mainloop.idle_add(Lang.bind(this, this._startupMountAll));
- },
-
- _InhibitorsChanged: function(object, senderName, [inhibtor]) {
- this._session.IsInhibitedRemote(GNOME_SESSION_AUTOMOUNT_INHIBIT,
- Lang.bind(this,
- function(result, error) {
- if (!error) {
- this._inhibited = result[0];
- }
- }));
- },
-
- _lockStatusChanged: function(shield, locked) {
- if (!locked) {
- this._volumeQueue.forEach(Lang.bind(this, function(volume) {
- this._checkAndMountVolume(volume);
- }));
- }
-
- // clear the queue anyway
- this._volumeQueue = [];
- },
-
- _startupMountAll: function() {
- let volumes = this._volumeMonitor.get_volumes();
- volumes.forEach(Lang.bind(this, function(volume) {
- this._checkAndMountVolume(volume, { checkSession: false,
- useMountOp: false,
- allowAutorun: false });
- }));
-
- return false;
- },
-
- _onDriveConnected: function() {
- // if we're not in the current ConsoleKit session,
- // or screensaver is active, don't play sounds
- if (!this._loginManager.sessionActive)
- return;
-
- if (Main.screenShield.locked)
- return;
-
- global.play_theme_sound(0, 'device-added-media');
- },
-
- _onDriveDisconnected: function() {
- // if we're not in the current ConsoleKit session,
- // or screensaver is active, don't play sounds
- if (!this._loginManager.sessionActive)
- return;
-
- if (Main.screenShield.locked)
- return;
-
- global.play_theme_sound(0, 'device-removed-media');
- },
-
- _onDriveEjectButton: function(monitor, drive) {
- // TODO: this code path is not tested, as the GVfs volume monitor
- // doesn't emit this signal just yet.
- if (!this._loginManager.sessionActive)
- return;
-
- // we force stop/eject in this case, so we don't have to pass a
- // mount operation object
- if (drive.can_stop()) {
- drive.stop
- (Gio.MountUnmountFlags.FORCE, null, null,
- Lang.bind(this, function(drive, res) {
- try {
- drive.stop_finish(res);
- } catch (e) {
- log("Unable to stop the drive after drive-eject-button " + e.toString());
- }
- }));
- } else if (drive.can_eject()) {
- drive.eject_with_operation
- (Gio.MountUnmountFlags.FORCE, null, null,
- Lang.bind(this, function(drive, res) {
- try {
- drive.eject_with_operation_finish(res);
- } catch (e) {
- log("Unable to eject the drive after drive-eject-button " + e.toString());
- }
- }));
- }
- },
-
- _onVolumeAdded: function(monitor, volume) {
- this._checkAndMountVolume(volume);
- },
-
- _checkAndMountVolume: function(volume, params) {
- params = Params.parse(params, { checkSession: true,
- useMountOp: true,
- allowAutorun: true });
-
- if (params.checkSession) {
- // if we're not in the current ConsoleKit session,
- // don't attempt automount
- if (!this._loginManager.sessionActive)
- return;
-
- if (Main.screenShield.locked) {
- if (this._volumeQueue.indexOf(volume) == -1)
- this._volumeQueue.push(volume);
-
- return;
- }
- }
-
- if (this._inhibited)
- return;
-
- // Volume is already mounted, don't bother.
- if (volume.get_mount())
- return;
-
- if (!this._settings.get_boolean(SETTING_ENABLE_AUTOMOUNT) ||
- !volume.should_automount() ||
- !volume.can_mount()) {
- // allow the autorun to run anyway; this can happen if the
- // mount gets added programmatically later, even if
- // should_automount() or can_mount() are false, like for
- // blank optical media.
- this._allowAutorun(volume);
- this._allowAutorunExpire(volume);
-
- return;
- }
-
- if (params.useMountOp) {
- let operation = new ShellMountOperation.ShellMountOperation(volume);
- this._mountVolume(volume, operation, params.allowAutorun);
- } else {
- this._mountVolume(volume, null, params.allowAutorun);
- }
- },
-
- _mountVolume: function(volume, operation, allowAutorun) {
- if (allowAutorun)
- this._allowAutorun(volume);
-
- let mountOp = operation ? operation.mountOp : null;
- volume._operation = operation;
-
- volume.mount(0, mountOp, null,
- Lang.bind(this, this._onVolumeMounted));
- },
-
- _onVolumeMounted: function(volume, res) {
- this._allowAutorunExpire(volume);
-
- try {
- volume.mount_finish(res);
- this._closeOperation(volume);
- } catch (e) {
- // FIXME: we will always get G_IO_ERROR_FAILED from the gvfs udisks
- // backend in this case, see
- // https://bugs.freedesktop.org/show_bug.cgi?id=51271
- if (e.message.indexOf('No key available with this passphrase') != -1) {
- this._reaskPassword(volume);
- } else {
- if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
- log('Unable to mount volume ' + volume.get_name() + ': ' + e.toString());
-
- this._closeOperation(volume);
- }
- }
- },
-
- _onVolumeRemoved: function(monitor, volume) {
- this._volumeQueue =
- this._volumeQueue.filter(function(element) {
- return (element != volume);
- });
- },
-
- _reaskPassword: function(volume) {
- let existingDialog = volume._operation ? volume._operation.borrowDialog() : null;
- let operation =
- new ShellMountOperation.ShellMountOperation(volume,
- { existingDialog: existingDialog });
- this._mountVolume(volume, operation);
- },
-
- _closeOperation: function(volume) {
- if (volume._operation)
- volume._operation.close();
- },
-
- _allowAutorun: function(volume) {
- volume.allowAutorun = true;
- },
-
- _allowAutorunExpire: function(volume) {
- Mainloop.timeout_add_seconds(AUTORUN_EXPIRE_TIMEOUT_SECS, function() {
- volume.allowAutorun = false;
- return false;
- });
- }
-});
+++ /dev/null
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-
-const Lang = imports.lang;
-const Gio = imports.gi.Gio;
-const St = imports.gi.St;
-
-const LoginManager = imports.misc.loginManager;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-const ShellMountOperation = imports.ui.shellMountOperation;
-
-// GSettings keys
-const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
-const SETTING_DISABLE_AUTORUN = 'autorun-never';
-const SETTING_START_APP = 'autorun-x-content-start-app';
-const SETTING_IGNORE = 'autorun-x-content-ignore';
-const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';
-
-const AutorunSetting = {
- RUN: 0,
- IGNORE: 1,
- FILES: 2,
- ASK: 3
-};
-
-// misc utils
-function shouldAutorunMount(mount, forTransient) {
- let root = mount.get_root();
- let volume = mount.get_volume();
-
- if (!volume || (!volume.allowAutorun && forTransient))
- return false;
-
- if (!root.is_native() || isMountRootHidden(root))
- return false;
-
- return true;
-}
-
-function isMountRootHidden(root) {
- let path = root.get_path();
-
- // skip any mounts in hidden directory hierarchies
- return (path.indexOf('/.') != -1);
-}
-
-function startAppForMount(app, mount) {
- let files = [];
- let root = mount.get_root();
- let retval = false;
-
- files.push(root);
-
- try {
- retval = app.launch(files,
- global.create_app_launch_context())
- } catch (e) {
- log('Unable to launch the application ' + app.get_name()
- + ': ' + e.toString());
- }
-
- return retval;
-}
-
-/******************************************/
-
-const HotplugSnifferIface = <interface name="org.gnome.Shell.HotplugSniffer">
-<method name="SniffURI">
- <arg type="s" direction="in" />
- <arg type="as" direction="out" />
-</method>
-</interface>;
-
-const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(HotplugSnifferIface);
-function HotplugSniffer() {
- return new HotplugSnifferProxy(Gio.DBus.session,
- 'org.gnome.Shell.HotplugSniffer',
- '/org/gnome/Shell/HotplugSniffer');
-}
-
-const ContentTypeDiscoverer = new Lang.Class({
- Name: 'ContentTypeDiscoverer',
-
- _init: function(callback) {
- this._callback = callback;
- },
-
- guessContentTypes: function(mount) {
- // guess mount's content types using GIO
- mount.guess_content_type(false, null,
- Lang.bind(this,
- this._onContentTypeGuessed));
- },
-
- _onContentTypeGuessed: function(mount, res) {
- let contentTypes = [];
-
- try {
- contentTypes = mount.guess_content_type_finish(res);
- } catch (e) {
- log('Unable to guess content types on added mount ' + mount.get_name()
- + ': ' + e.toString());
- }
-
- if (contentTypes.length) {
- this._emitCallback(mount, contentTypes);
- } else {
- let root = mount.get_root();
-
- let hotplugSniffer = new HotplugSniffer();
- hotplugSniffer.SniffURIRemote(root.get_uri(),
- Lang.bind(this, function([contentTypes]) {
- this._emitCallback(mount, contentTypes);
- }));
- }
- },
-
- _emitCallback: function(mount, contentTypes) {
- if (!contentTypes)
- contentTypes = [];
-
- // we're not interested in win32 software content types here
- contentTypes = contentTypes.filter(function(type) {
- return (type != 'x-content/win32-software');
- });
-
- let apps = [];
- contentTypes.forEach(function(type) {
- let app = Gio.app_info_get_default_for_type(type, false);
-
- if (app)
- apps.push(app);
- });
-
- if (apps.length == 0)
- apps.push(Gio.app_info_get_default_for_type('inode/directory', false));
-
- this._callback(mount, apps, contentTypes);
- }
-});
-
-const AutorunManager = new Lang.Class({
- Name: 'AutorunManager',
-
- _init: function() {
- this._loginManager = LoginManager.getLoginManager();
-
- this._volumeMonitor = Gio.VolumeMonitor.get();
-
- this._volumeMonitor.connect('mount-added',
- Lang.bind(this,
- this._onMountAdded));
- this._volumeMonitor.connect('mount-removed',
- Lang.bind(this,
- this._onMountRemoved));
-
- this._transDispatcher = new AutorunTransientDispatcher();
- this._createResidentSource();
-
- let mounts = this._volumeMonitor.get_mounts();
-
- mounts.forEach(Lang.bind(this, function (mount) {
- let discoverer = new ContentTypeDiscoverer(Lang.bind (this,
- function (mount, apps) {
- this._residentSource.addMount(mount, apps);
- }));
-
- discoverer.guessContentTypes(mount);
- }));
- },
-
- _createResidentSource: function() {
- this._residentSource = new AutorunResidentSource();
- this._residentSource.connect('destroy',
- Lang.bind(this,
- this._createResidentSource));
- },
-
- _onMountAdded: function(monitor, mount) {
- // don't do anything if our session is not the currently
- // active one
- if (!this._loginManager.sessionActive)
- return;
-
- let discoverer = new ContentTypeDiscoverer(Lang.bind (this,
- function (mount, apps, contentTypes) {
- this._transDispatcher.addMount(mount, apps, contentTypes);
- this._residentSource.addMount(mount, apps);
- }));
-
- discoverer.guessContentTypes(mount);
- },
-
- _onMountRemoved: function(monitor, mount) {
- this._transDispatcher.removeMount(mount);
- this._residentSource.removeMount(mount);
- },
-
- ejectMount: function(mount) {
- let mountOp = new ShellMountOperation.ShellMountOperation(mount);
-
- // first, see if we have a drive
- let drive = mount.get_drive();
- let volume = mount.get_volume();
-
- if (drive &&
- drive.get_start_stop_type() == Gio.DriveStartStopType.SHUTDOWN &&
- drive.can_stop()) {
- drive.stop(0, mountOp.mountOp, null,
- Lang.bind(this, this._onStop));
- } else {
- if (mount.can_eject()) {
- mount.eject_with_operation(0, mountOp.mountOp, null,
- Lang.bind(this, this._onEject));
- } else if (volume && volume.can_eject()) {
- volume.eject_with_operation(0, mountOp.mountOp, null,
- Lang.bind(this, this._onEject));
- } else if (drive && drive.can_eject()) {
- drive.eject_with_operation(0, mountOp.mountOp, null,
- Lang.bind(this, this._onEject));
- } else if (mount.can_unmount()) {
- mount.unmount_with_operation(0, mountOp.mountOp, null,
- Lang.bind(this, this._onUnmount));
- }
- }
- },
-
- _onUnmount: function(mount, res) {
- try {
- mount.unmount_with_operation_finish(res);
- } catch (e) {
- if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
- log('Unable to eject the mount ' + mount.get_name()
- + ': ' + e.toString());
- }
- },
-
- _onEject: function(source, res) {
- try {
- source.eject_with_operation_finish(res);
- } catch (e) {
- if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
- log('Unable to eject the drive ' + source.get_name()
- + ': ' + e.toString());
- }
- },
-
- _onStop: function(drive, res) {
- try {
- drive.stop_finish(res);
- } catch (e) {
- if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
- log('Unable to stop the drive ' + drive.get_name()
- + ': ' + e.toString());
- }
- },
-});
-
-const AutorunResidentSource = new Lang.Class({
- Name: 'AutorunResidentSource',
- Extends: MessageTray.Source,
-
- _init: function() {
- this.parent(_("Removable Devices"), 'media-removable');
- this.showInLockScreen = false;
-
- this._mounts = [];
-
- this._notification = new AutorunResidentNotification(this);
- },
-
- addMount: function(mount, apps) {
- if (!shouldAutorunMount(mount, false))
- return;
-
- let filtered = this._mounts.filter(function (element) {
- return (element.mount == mount);
- });
-
- if (filtered.length != 0)
- return;
-
- let element = { mount: mount, apps: apps };
- this._mounts.push(element);
- this._redisplay();
- },
-
- removeMount: function(mount) {
- this._mounts =
- this._mounts.filter(function (element) {
- return (element.mount != mount);
- });
-
- this._redisplay();
- },
-
- _redisplay: function() {
- if (this._mounts.length == 0) {
- this._notification.destroy();
- this.destroy();
-
- return;
- }
-
- this._notification.updateForMounts(this._mounts);
-
- // add ourselves as a source, and push the notification
- if (!Main.messageTray.contains(this)) {
- Main.messageTray.add(this);
- this.pushNotification(this._notification);
- }
- }
-});
-
-const AutorunResidentNotification = new Lang.Class({
- Name: 'AutorunResidentNotification',
- Extends: MessageTray.Notification,
-
- _init: function(source) {
- this.parent(source, source.title, null, { customContent: true });
-
- // set the notification as resident
- this.setResident(true);
-
- this._layout = new St.BoxLayout ({ style_class: 'hotplug-resident-box',
- vertical: true });
-
- this.addActor(this._layout,
- { x_expand: true,
- x_fill: true });
- },
-
- updateForMounts: function(mounts) {
- // remove all the layout content
- this._layout.destroy_all_children();
-
- for (let idx = 0; idx < mounts.length; idx++) {
- let element = mounts[idx];
-
- let actor = this._itemForMount(element.mount, element.apps);
- this._layout.add(actor, { x_fill: true,
- expand: true });
- }
- },
-
- _itemForMount: function(mount, apps) {
- let item = new St.BoxLayout();
-
- // prepare the mount button content
- let mountLayout = new St.BoxLayout();
-
- let mountIcon = new St.Icon({ gicon: mount.get_icon(),
- style_class: 'hotplug-resident-mount-icon' });
- mountLayout.add_actor(mountIcon);
-
- let labelBin = new St.Bin({ y_align: St.Align.MIDDLE });
- let mountLabel =
- new St.Label({ text: mount.get_name(),
- style_class: 'hotplug-resident-mount-label',
- track_hover: true,
- reactive: true });
- labelBin.add_actor(mountLabel);
- mountLayout.add_actor(labelBin);
-
- let mountButton = new St.Button({ child: mountLayout,
- x_align: St.Align.START,
- x_fill: true,
- style_class: 'hotplug-resident-mount',
- button_mask: St.ButtonMask.ONE });
- item.add(mountButton, { x_align: St.Align.START,
- expand: true });
-
- let ejectIcon =
- new St.Icon({ icon_name: 'media-eject',
- style_class: 'hotplug-resident-eject-icon' });
-
- let ejectButton =
- new St.Button({ style_class: 'hotplug-resident-eject-button',
- button_mask: St.ButtonMask.ONE,
- child: ejectIcon });
- item.add(ejectButton, { x_align: St.Align.END });
-
- // now connect signals
- mountButton.connect('clicked', Lang.bind(this, function(actor, event) {
- startAppForMount(apps[0], mount);
- }));
-
- ejectButton.connect('clicked', Lang.bind(this, function() {
- Main.autorunManager.ejectMount(mount);
- }));
-
- return item;
- },
-});
-
-const AutorunTransientDispatcher = new Lang.Class({
- Name: 'AutorunTransientDispatcher',
-
- _init: function() {
- this._sources = [];
- this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
- },
-
- _getAutorunSettingForType: function(contentType) {
- let runApp = this._settings.get_strv(SETTING_START_APP);
- if (runApp.indexOf(contentType) != -1)
- return AutorunSetting.RUN;
-
- let ignore = this._settings.get_strv(SETTING_IGNORE);
- if (ignore.indexOf(contentType) != -1)
- return AutorunSetting.IGNORE;
-
- let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER);
- if (openFiles.indexOf(contentType) != -1)
- return AutorunSetting.FILES;
-
- return AutorunSetting.ASK;
- },
-
- _getSourceForMount: function(mount) {
- let filtered =
- this._sources.filter(function (source) {
- return (source.mount == mount);
- });
-
- // we always make sure not to add two sources for the same
- // mount in addMount(), so it's safe to assume filtered.length
- // is always either 1 or 0.
- if (filtered.length == 1)
- return filtered[0];
-
- return null;
- },
-
- _addSource: function(mount, apps) {
- // if we already have a source showing for this
- // mount, return
- if (this._getSourceForMount(mount))
- return;
-
- // add a new source
- this._sources.push(new AutorunTransientSource(mount, apps));
- },
-
- addMount: function(mount, apps, contentTypes) {
- // if autorun is disabled globally, return
- if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
- return;
-
- // if the mount doesn't want to be autorun, return
- if (!shouldAutorunMount(mount, true))
- return;
-
- let setting = this._getAutorunSettingForType(contentTypes[0]);
-
- // check at the settings for the first content type
- // to see whether we should ask
- if (setting == AutorunSetting.IGNORE)
- return; // return right away
-
- let success = false;
- let app = null;
-
- if (setting == AutorunSetting.RUN) {
- app = Gio.app_info_get_default_for_type(contentTypes[0], false);
- } else if (setting == AutorunSetting.FILES) {
- app = Gio.app_info_get_default_for_type('inode/directory', false);
- }
-
- if (app)
- success = startAppForMount(app, mount);
-
- // we fallback here also in case the settings did not specify 'ask',
- // but we failed launching the default app or the default file manager
- if (!success)
- this._addSource(mount, apps);
- },
-
- removeMount: function(mount) {
- let source = this._getSourceForMount(mount);
-
- // if we aren't tracking this mount, don't do anything
- if (!source)
- return;
-
- // destroy the notification source
- source.destroy();
- }
-});
-
-const AutorunTransientSource = new Lang.Class({
- Name: 'AutorunTransientSource',
- Extends: MessageTray.Source,
-
- _init: function(mount, apps) {
- this.mount = mount;
- this.apps = apps;
-
- this.parent(mount.get_name());
-
- this._notification = new AutorunTransientNotification(this);
-
- // add ourselves as a source, and popup the notification
- Main.messageTray.add(this);
- this.notify(this._notification);
- },
-
- createIcon: function(size) {
- return new St.Icon({ gicon: this.mount.get_icon(),
- icon_size: size });
- }
-});
-
-const AutorunTransientNotification = new Lang.Class({
- Name: 'AutorunTransientNotification',
- Extends: MessageTray.Notification,
-
- _init: function(source) {
- this.parent(source, source.title, null, { customContent: true });
-
- this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box',
- vertical: true });
- this.addActor(this._box);
-
- this._mount = source.mount;
-
- source.apps.forEach(Lang.bind(this, function (app) {
- let actor = this._buttonForApp(app);
-
- if (actor)
- this._box.add(actor, { x_fill: true,
- x_align: St.Align.START });
- }));
-
- this._box.add(this._buttonForEject(), { x_fill: true,
- x_align: St.Align.START });
-
- // set the notification to transient and urgent, so that it
- // expands out
- this.setTransient(true);
- this.setUrgency(MessageTray.Urgency.CRITICAL);
- },
-
- _buttonForApp: function(app) {
- let box = new St.BoxLayout();
- let icon = new St.Icon({ gicon: app.get_icon(),
- style_class: 'hotplug-notification-item-icon' });
- box.add(icon);
-
- let label = new St.Bin({ y_align: St.Align.MIDDLE,
- child: new St.Label
- ({ text: _("Open with %s").format(app.get_name()) })
- });
- box.add(label);
-
- let button = new St.Button({ child: box,
- x_fill: true,
- x_align: St.Align.START,
- button_mask: St.ButtonMask.ONE,
- style_class: 'hotplug-notification-item' });
-
- button.connect('clicked', Lang.bind(this, function() {
- startAppForMount(app, this._mount);
- this.destroy();
- }));
-
- return button;
- },
-
- _buttonForEject: function() {
- let box = new St.BoxLayout();
- let icon = new St.Icon({ icon_name: 'media-eject',
- style_class: 'hotplug-notification-item-icon' });
- box.add(icon);
-
- let label = new St.Bin({ y_align: St.Align.MIDDLE,
- child: new St.Label
- ({ text: _("Eject") })
- });
- box.add(label);
-
- let button = new St.Button({ child: box,
- x_fill: true,
- x_align: St.Align.START,
- button_mask: St.ButtonMask.ONE,
- style_class: 'hotplug-notification-item' });
-
- button.connect('clicked', Lang.bind(this, function() {
- Main.autorunManager.ejectMount(this._mount);
- }));
-
- return button;
- }
-});
-
--- /dev/null
+
+const Lang = imports.lang;
+const Main = imports.ui.main;
+
+const ComponentManager = new Lang.Class({
+ Name: 'ComponentManager',
+
+ _init: function() {
+ this._allComponents = {};
+ this._enabledComponents = [];
+
+ Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated));
+ this._sessionUpdated();
+ },
+
+ _sessionUpdated: function() {
+ let newEnabledComponents = Main.sessionMode.components;
+
+ newEnabledComponents.filter(Lang.bind(this, function(name) {
+ return this._enabledComponents.indexOf(name) == -1;
+ })).forEach(Lang.bind(this, function(name) {
+ this._enableComponent(name);
+ }));
+
+ this._enabledComponents.filter(Lang.bind(this, function(name) {
+ return newEnabledComponents.indexOf(name) == -1;
+ })).forEach(Lang.bind(this, function(name) {
+ this._disableComponent(name);
+ }));
+
+ this._enabledComponents = newEnabledComponents;
+ },
+
+ _importComponent: function(name) {
+ let module = imports.ui.components[name];
+ return module.Component;
+ },
+
+ _ensureComponent: function(name) {
+ let component = this._allComponents[name];
+ if (component)
+ return component;
+
+ let constructor = this._importComponent(name);
+ component = new constructor();
+ this._allComponents[name] = component;
+ return component;
+ },
+
+ _enableComponent: function(name) {
+ let component = this._ensureComponent(name);
+ component.enable();
+ },
+
+ _disableComponent: function(name) {
+ let component = this._allComponents[name];
+ if (component == null)
+ return;
+ component.disable();
+ }
+});
--- /dev/null
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Params = imports.misc.params;
+const Shell = imports.gi.Shell;
+
+const GnomeSession = imports.misc.gnomeSession;
+const LoginManager = imports.misc.loginManager;
+const Main = imports.ui.main;
+const ShellMountOperation = imports.ui.shellMountOperation;
+
+const GNOME_SESSION_AUTOMOUNT_INHIBIT = 16;
+
+// GSettings keys
+const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
+const SETTING_ENABLE_AUTOMOUNT = 'automount';
+
+const AUTORUN_EXPIRE_TIMEOUT_SECS = 10;
+
+const AutomountManager = new Lang.Class({
+ Name: 'AutomountManager',
+
+ _init: function() {
+ this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
+ this._volumeQueue = [];
+ this._session = new GnomeSession.SessionManager();
+ this._session.connectSignal('InhibitorAdded',
+ Lang.bind(this, this._InhibitorsChanged));
+ this._session.connectSignal('InhibitorRemoved',
+ Lang.bind(this, this._InhibitorsChanged));
+ this._inhibited = false;
+
+ this._loginManager = LoginManager.getLoginManager();
+ this._volumeMonitor = Gio.VolumeMonitor.get();
+ },
+
+ enable: function() {
+ this._volumeAddedId = this._volumeMonitor.connect('volume-added', Lang.bind(this, this._onVolumeAdded));
+ this._volumeRemovedId = this._volumeMonitor.connect('volume-removed', Lang.bind(this, this._onVolumeRemoved));
+ this._driveConnectedId = this._volumeMonitor.connect('drive-connected', Lang.bind(this, this._onDriveConnected));
+ this._driveDisconnectedId = this._volumeMonitor.connect('drive-disconnected', Lang.bind(this, this._onDriveDisconnected));
+ this._driveEjectButtonId = this._volumeMonitor.connect('drive-eject-button', Lang.bind(this, this._onDriveEjectButton));
+
+ this._mountAllId = Mainloop.idle_add(Lang.bind(this, this._startupMountAll));
+ },
+
+ disable: function() {
+ this._volumeMonitor.disconnect(this._volumeAddedId);
+ this._volumeMonitor.disconnect(this._volumeRemovedId);
+ this._volumeMonitor.disconnect(this._driveConnectedId);
+ this._volumeMonitor.disconnect(this._driveDisconnectedId);
+ this._volumeMonitor.disconnect(this._driveEjectButtonId);
+
+ if (this._mountAllId > 0) {
+ Mainloop.source_remove(this._mountAllId);
+ this._mountAllId = 0;
+ }
+ },
+
+ _InhibitorsChanged: function(object, senderName, [inhibtor]) {
+ this._session.IsInhibitedRemote(GNOME_SESSION_AUTOMOUNT_INHIBIT,
+ Lang.bind(this,
+ function(result, error) {
+ if (!error) {
+ this._inhibited = result[0];
+ }
+ }));
+ },
+
+ _startupMountAll: function() {
+ let volumes = this._volumeMonitor.get_volumes();
+ volumes.forEach(Lang.bind(this, function(volume) {
+ this._checkAndMountVolume(volume, { checkSession: false,
+ useMountOp: false,
+ allowAutorun: false });
+ }));
+
+ this._mountAllId = 0;
+ return false;
+ },
+
+ _onDriveConnected: function() {
+ // if we're not in the current ConsoleKit session,
+ // or screensaver is active, don't play sounds
+ if (!this._loginManager.sessionActive)
+ return;
+
+ global.play_theme_sound(0, 'device-added-media');
+ },
+
+ _onDriveDisconnected: function() {
+ // if we're not in the current ConsoleKit session,
+ // or screensaver is active, don't play sounds
+ if (!this._loginManager.sessionActive)
+ return;
+
+ global.play_theme_sound(0, 'device-removed-media');
+ },
+
+ _onDriveEjectButton: function(monitor, drive) {
+ // TODO: this code path is not tested, as the GVfs volume monitor
+ // doesn't emit this signal just yet.
+ if (!this._loginManager.sessionActive)
+ return;
+
+ // we force stop/eject in this case, so we don't have to pass a
+ // mount operation object
+ if (drive.can_stop()) {
+ drive.stop
+ (Gio.MountUnmountFlags.FORCE, null, null,
+ Lang.bind(this, function(drive, res) {
+ try {
+ drive.stop_finish(res);
+ } catch (e) {
+ log("Unable to stop the drive after drive-eject-button " + e.toString());
+ }
+ }));
+ } else if (drive.can_eject()) {
+ drive.eject_with_operation
+ (Gio.MountUnmountFlags.FORCE, null, null,
+ Lang.bind(this, function(drive, res) {
+ try {
+ drive.eject_with_operation_finish(res);
+ } catch (e) {
+ log("Unable to eject the drive after drive-eject-button " + e.toString());
+ }
+ }));
+ }
+ },
+
+ _onVolumeAdded: function(monitor, volume) {
+ this._checkAndMountVolume(volume);
+ },
+
+ _checkAndMountVolume: function(volume, params) {
+ params = Params.parse(params, { checkSession: true,
+ useMountOp: true,
+ allowAutorun: true });
+
+ if (params.checkSession) {
+ // if we're not in the current ConsoleKit session,
+ // don't attempt automount
+ if (!this._loginManager.sessionActive)
+ return;
+ }
+
+ if (this._inhibited)
+ return;
+
+ // Volume is already mounted, don't bother.
+ if (volume.get_mount())
+ return;
+
+ if (!this._settings.get_boolean(SETTING_ENABLE_AUTOMOUNT) ||
+ !volume.should_automount() ||
+ !volume.can_mount()) {
+ // allow the autorun to run anyway; this can happen if the
+ // mount gets added programmatically later, even if
+ // should_automount() or can_mount() are false, like for
+ // blank optical media.
+ this._allowAutorun(volume);
+ this._allowAutorunExpire(volume);
+
+ return;
+ }
+
+ if (params.useMountOp) {
+ let operation = new ShellMountOperation.ShellMountOperation(volume);
+ this._mountVolume(volume, operation, params.allowAutorun);
+ } else {
+ this._mountVolume(volume, null, params.allowAutorun);
+ }
+ },
+
+ _mountVolume: function(volume, operation, allowAutorun) {
+ if (allowAutorun)
+ this._allowAutorun(volume);
+
+ let mountOp = operation ? operation.mountOp : null;
+ volume._operation = operation;
+
+ volume.mount(0, mountOp, null,
+ Lang.bind(this, this._onVolumeMounted));
+ },
+
+ _onVolumeMounted: function(volume, res) {
+ this._allowAutorunExpire(volume);
+
+ try {
+ volume.mount_finish(res);
+ this._closeOperation(volume);
+ } catch (e) {
+ // FIXME: we will always get G_IO_ERROR_FAILED from the gvfs udisks
+ // backend in this case, see
+ // https://bugs.freedesktop.org/show_bug.cgi?id=51271
+ if (e.message.indexOf('No key available with this passphrase') != -1) {
+ this._reaskPassword(volume);
+ } else {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+ log('Unable to mount volume ' + volume.get_name() + ': ' + e.toString());
+
+ this._closeOperation(volume);
+ }
+ }
+ },
+
+ _onVolumeRemoved: function(monitor, volume) {
+ this._volumeQueue =
+ this._volumeQueue.filter(function(element) {
+ return (element != volume);
+ });
+ },
+
+ _reaskPassword: function(volume) {
+ let existingDialog = volume._operation ? volume._operation.borrowDialog() : null;
+ let operation =
+ new ShellMountOperation.ShellMountOperation(volume,
+ { existingDialog: existingDialog });
+ this._mountVolume(volume, operation);
+ },
+
+ _closeOperation: function(volume) {
+ if (volume._operation)
+ volume._operation.close();
+ },
+
+ _allowAutorun: function(volume) {
+ volume.allowAutorun = true;
+ },
+
+ _allowAutorunExpire: function(volume) {
+ Mainloop.timeout_add_seconds(AUTORUN_EXPIRE_TIMEOUT_SECS, function() {
+ volume.allowAutorun = false;
+ return false;
+ });
+ }
+});
+const Component = AutomountManager;
--- /dev/null
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Lang = imports.lang;
+const Gio = imports.gi.Gio;
+const St = imports.gi.St;
+
+const LoginManager = imports.misc.loginManager;
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+const ShellMountOperation = imports.ui.shellMountOperation;
+
+// GSettings keys
+const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
+const SETTING_DISABLE_AUTORUN = 'autorun-never';
+const SETTING_START_APP = 'autorun-x-content-start-app';
+const SETTING_IGNORE = 'autorun-x-content-ignore';
+const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';
+
+const AutorunSetting = {
+ RUN: 0,
+ IGNORE: 1,
+ FILES: 2,
+ ASK: 3
+};
+
+// misc utils
+function shouldAutorunMount(mount, forTransient) {
+ let root = mount.get_root();
+ let volume = mount.get_volume();
+
+ if (!volume || (!volume.allowAutorun && forTransient))
+ return false;
+
+ if (!root.is_native() || isMountRootHidden(root))
+ return false;
+
+ return true;
+}
+
+function isMountRootHidden(root) {
+ let path = root.get_path();
+
+ // skip any mounts in hidden directory hierarchies
+ return (path.indexOf('/.') != -1);
+}
+
+function startAppForMount(app, mount) {
+ let files = [];
+ let root = mount.get_root();
+ let retval = false;
+
+ files.push(root);
+
+ try {
+ retval = app.launch(files,
+ global.create_app_launch_context())
+ } catch (e) {
+ log('Unable to launch the application ' + app.get_name()
+ + ': ' + e.toString());
+ }
+
+ return retval;
+}
+
+/******************************************/
+
+const HotplugSnifferIface = <interface name="org.gnome.Shell.HotplugSniffer">
+<method name="SniffURI">
+ <arg type="s" direction="in" />
+ <arg type="as" direction="out" />
+</method>
+</interface>;
+
+const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(HotplugSnifferIface);
+function HotplugSniffer() {
+ return new HotplugSnifferProxy(Gio.DBus.session,
+ 'org.gnome.Shell.HotplugSniffer',
+ '/org/gnome/Shell/HotplugSniffer');
+}
+
+const ContentTypeDiscoverer = new Lang.Class({
+ Name: 'ContentTypeDiscoverer',
+
+ _init: function(callback) {
+ this._callback = callback;
+ },
+
+ guessContentTypes: function(mount) {
+ // guess mount's content types using GIO
+ mount.guess_content_type(false, null,
+ Lang.bind(this,
+ this._onContentTypeGuessed));
+ },
+
+ _onContentTypeGuessed: function(mount, res) {
+ let contentTypes = [];
+
+ try {
+ contentTypes = mount.guess_content_type_finish(res);
+ } catch (e) {
+ log('Unable to guess content types on added mount ' + mount.get_name()
+ + ': ' + e.toString());
+ }
+
+ if (contentTypes.length) {
+ this._emitCallback(mount, contentTypes);
+ } else {
+ let root = mount.get_root();
+
+ let hotplugSniffer = new HotplugSniffer();
+ hotplugSniffer.SniffURIRemote(root.get_uri(),
+ Lang.bind(this, function([contentTypes]) {
+ this._emitCallback(mount, contentTypes);
+ }));
+ }
+ },
+
+ _emitCallback: function(mount, contentTypes) {
+ if (!contentTypes)
+ contentTypes = [];
+
+ // we're not interested in win32 software content types here
+ contentTypes = contentTypes.filter(function(type) {
+ return (type != 'x-content/win32-software');
+ });
+
+ let apps = [];
+ contentTypes.forEach(function(type) {
+ let app = Gio.app_info_get_default_for_type(type, false);
+
+ if (app)
+ apps.push(app);
+ });
+
+ if (apps.length == 0)
+ apps.push(Gio.app_info_get_default_for_type('inode/directory', false));
+
+ this._callback(mount, apps, contentTypes);
+ }
+});
+
+const AutorunManager = new Lang.Class({
+ Name: 'AutorunManager',
+
+ _init: function() {
+ this._loginManager = LoginManager.getLoginManager();
+
+ this._volumeMonitor = Gio.VolumeMonitor.get();
+
+ this._transDispatcher = new AutorunTransientDispatcher(this);
+ },
+
+ enable: function() {
+ this._residentSource = new AutorunResidentSource(this);
+ this._scanMounts();
+
+ this._mountAddedId = this._volumeMonitor.connect('mount-added', Lang.bind(this, this._onMountAdded));
+ this._mountRemovedId = this._volumeMonitor.connect('mount-removed', Lang.bind(this, this._onMountRemoved));
+ },
+
+ disable: function() {
+ this._residentSource.destroy();
+ this._volumeMonitor.disconnect(this._mountAddedId);
+ this._volumeMonitor.disconnect(this._mountRemovedId);
+ },
+
+ _scanMounts: function() {
+ let mounts = this._volumeMonitor.get_mounts();
+ mounts.forEach(Lang.bind(this, function (mount) {
+ let discoverer = new ContentTypeDiscoverer(Lang.bind (this,
+ function (mount, apps) {
+ this._residentSource.addMount(mount, apps);
+ }));
+
+ discoverer.guessContentTypes(mount);
+ }));
+ },
+
+ _onMountAdded: function(monitor, mount) {
+ // don't do anything if our session is not the currently
+ // active one
+ if (!this._loginManager.sessionActive)
+ return;
+
+ let discoverer = new ContentTypeDiscoverer(Lang.bind (this,
+ function (mount, apps, contentTypes) {
+ this._transDispatcher.addMount(mount, apps, contentTypes);
+ this._residentSource.addMount(mount, apps);
+ }));
+
+ discoverer.guessContentTypes(mount);
+ },
+
+ _onMountRemoved: function(monitor, mount) {
+ this._transDispatcher.removeMount(mount);
+ this._residentSource.removeMount(mount);
+ },
+
+ ejectMount: function(mount) {
+ let mountOp = new ShellMountOperation.ShellMountOperation(mount);
+
+ // first, see if we have a drive
+ let drive = mount.get_drive();
+ let volume = mount.get_volume();
+
+ if (drive &&
+ drive.get_start_stop_type() == Gio.DriveStartStopType.SHUTDOWN &&
+ drive.can_stop()) {
+ drive.stop(0, mountOp.mountOp, null,
+ Lang.bind(this, this._onStop));
+ } else {
+ if (mount.can_eject()) {
+ mount.eject_with_operation(0, mountOp.mountOp, null,
+ Lang.bind(this, this._onEject));
+ } else if (volume && volume.can_eject()) {
+ volume.eject_with_operation(0, mountOp.mountOp, null,
+ Lang.bind(this, this._onEject));
+ } else if (drive && drive.can_eject()) {
+ drive.eject_with_operation(0, mountOp.mountOp, null,
+ Lang.bind(this, this._onEject));
+ } else if (mount.can_unmount()) {
+ mount.unmount_with_operation(0, mountOp.mountOp, null,
+ Lang.bind(this, this._onUnmount));
+ }
+ }
+ },
+
+ _onUnmount: function(mount, res) {
+ try {
+ mount.unmount_with_operation_finish(res);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+ log('Unable to eject the mount ' + mount.get_name()
+ + ': ' + e.toString());
+ }
+ },
+
+ _onEject: function(source, res) {
+ try {
+ source.eject_with_operation_finish(res);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+ log('Unable to eject the drive ' + source.get_name()
+ + ': ' + e.toString());
+ }
+ },
+
+ _onStop: function(drive, res) {
+ try {
+ drive.stop_finish(res);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+ log('Unable to stop the drive ' + drive.get_name()
+ + ': ' + e.toString());
+ }
+ },
+});
+
+const AutorunResidentSource = new Lang.Class({
+ Name: 'AutorunResidentSource',
+ Extends: MessageTray.Source,
+
+ _init: function(manager) {
+ this.parent(_("Removable Devices"), 'media-removable');
+ this.showInLockScreen = false;
+
+ this._mounts = [];
+
+ this._notification = new AutorunResidentNotification(this._manager, this);
+ this._manager = manager;
+ },
+
+ addMount: function(mount, apps) {
+ if (!shouldAutorunMount(mount, false))
+ return;
+
+ let filtered = this._mounts.filter(function (element) {
+ return (element.mount == mount);
+ });
+
+ if (filtered.length != 0)
+ return;
+
+ let element = { mount: mount, apps: apps };
+ this._mounts.push(element);
+ this._redisplay();
+ },
+
+ removeMount: function(mount) {
+ this._mounts =
+ this._mounts.filter(function (element) {
+ return (element.mount != mount);
+ });
+
+ this._redisplay();
+ },
+
+ _redisplay: function() {
+ if (this._mounts.length == 0) {
+ this._notification.destroy();
+ this.destroy();
+
+ return;
+ }
+
+ this._notification.updateForMounts(this._mounts);
+
+ // add ourselves as a source, and push the notification
+ if (!Main.messageTray.contains(this)) {
+ Main.messageTray.add(this);
+ this.pushNotification(this._notification);
+ }
+ }
+});
+
+const AutorunResidentNotification = new Lang.Class({
+ Name: 'AutorunResidentNotification',
+ Extends: MessageTray.Notification,
+
+ _init: function(manager, source) {
+ this.parent(source, source.title, null, { customContent: true });
+
+ // set the notification as resident
+ this.setResident(true);
+
+ this._layout = new St.BoxLayout ({ style_class: 'hotplug-resident-box',
+ vertical: true });
+ this._manager = manager;
+
+ this.addActor(this._layout,
+ { x_expand: true,
+ x_fill: true });
+ },
+
+ updateForMounts: function(mounts) {
+ // remove all the layout content
+ this._layout.destroy_all_children();
+
+ for (let idx = 0; idx < mounts.length; idx++) {
+ let element = mounts[idx];
+
+ let actor = this._itemForMount(element.mount, element.apps);
+ this._layout.add(actor, { x_fill: true,
+ expand: true });
+ }
+ },
+
+ _itemForMount: function(mount, apps) {
+ let item = new St.BoxLayout();
+
+ // prepare the mount button content
+ let mountLayout = new St.BoxLayout();
+
+ let mountIcon = new St.Icon({ gicon: mount.get_icon(),
+ style_class: 'hotplug-resident-mount-icon' });
+ mountLayout.add_actor(mountIcon);
+
+ let labelBin = new St.Bin({ y_align: St.Align.MIDDLE });
+ let mountLabel =
+ new St.Label({ text: mount.get_name(),
+ style_class: 'hotplug-resident-mount-label',
+ track_hover: true,
+ reactive: true });
+ labelBin.add_actor(mountLabel);
+ mountLayout.add_actor(labelBin);
+
+ let mountButton = new St.Button({ child: mountLayout,
+ x_align: St.Align.START,
+ x_fill: true,
+ style_class: 'hotplug-resident-mount',
+ button_mask: St.ButtonMask.ONE });
+ item.add(mountButton, { x_align: St.Align.START,
+ expand: true });
+
+ let ejectIcon =
+ new St.Icon({ icon_name: 'media-eject',
+ style_class: 'hotplug-resident-eject-icon' });
+
+ let ejectButton =
+ new St.Button({ style_class: 'hotplug-resident-eject-button',
+ button_mask: St.ButtonMask.ONE,
+ child: ejectIcon });
+ item.add(ejectButton, { x_align: St.Align.END });
+
+ // now connect signals
+ mountButton.connect('clicked', Lang.bind(this, function(actor, event) {
+ this._manager.startAppForMount(apps[0], mount);
+ }));
+
+ ejectButton.connect('clicked', Lang.bind(this, function() {
+ this._manager.ejectMount(mount);
+ }));
+
+ return item;
+ },
+});
+
+const AutorunTransientDispatcher = new Lang.Class({
+ Name: 'AutorunTransientDispatcher',
+
+ _init: function(manager) {
+ this._manager = manager;
+ this._sources = [];
+ this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
+ },
+
+ _getAutorunSettingForType: function(contentType) {
+ let runApp = this._settings.get_strv(SETTING_START_APP);
+ if (runApp.indexOf(contentType) != -1)
+ return AutorunSetting.RUN;
+
+ let ignore = this._settings.get_strv(SETTING_IGNORE);
+ if (ignore.indexOf(contentType) != -1)
+ return AutorunSetting.IGNORE;
+
+ let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER);
+ if (openFiles.indexOf(contentType) != -1)
+ return AutorunSetting.FILES;
+
+ return AutorunSetting.ASK;
+ },
+
+ _getSourceForMount: function(mount) {
+ let filtered =
+ this._sources.filter(function (source) {
+ return (source.mount == mount);
+ });
+
+ // we always make sure not to add two sources for the same
+ // mount in addMount(), so it's safe to assume filtered.length
+ // is always either 1 or 0.
+ if (filtered.length == 1)
+ return filtered[0];
+
+ return null;
+ },
+
+ _addSource: function(mount, apps) {
+ // if we already have a source showing for this
+ // mount, return
+ if (this._getSourceForMount(mount))
+ return;
+
+ // add a new source
+ this._sources.push(new AutorunTransientSource(this._manager, mount, apps));
+ },
+
+ addMount: function(mount, apps, contentTypes) {
+ // if autorun is disabled globally, return
+ if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
+ return;
+
+ // if the mount doesn't want to be autorun, return
+ if (!shouldAutorunMount(mount, true))
+ return;
+
+ let setting = this._getAutorunSettingForType(contentTypes[0]);
+
+ // check at the settings for the first content type
+ // to see whether we should ask
+ if (setting == AutorunSetting.IGNORE)
+ return; // return right away
+
+ let success = false;
+ let app = null;
+
+ if (setting == AutorunSetting.RUN) {
+ app = Gio.app_info_get_default_for_type(contentTypes[0], false);
+ } else if (setting == AutorunSetting.FILES) {
+ app = Gio.app_info_get_default_for_type('inode/directory', false);
+ }
+
+ if (app)
+ success = startAppForMount(app, mount);
+
+ // we fallback here also in case the settings did not specify 'ask',
+ // but we failed launching the default app or the default file manager
+ if (!success)
+ this._addSource(mount, apps);
+ },
+
+ removeMount: function(mount) {
+ let source = this._getSourceForMount(mount);
+
+ // if we aren't tracking this mount, don't do anything
+ if (!source)
+ return;
+
+ // destroy the notification source
+ source.destroy();
+ }
+});
+
+const AutorunTransientSource = new Lang.Class({
+ Name: 'AutorunTransientSource',
+ Extends: MessageTray.Source,
+
+ _init: function(manager, mount, apps) {
+ this._manager = manager;
+ this.mount = mount;
+ this.apps = apps;
+
+ this.parent(mount.get_name());
+
+ this._notification = new AutorunTransientNotification(this._manager, this);
+
+ // add ourselves as a source, and popup the notification
+ Main.messageTray.add(this);
+ this.notify(this._notification);
+ },
+
+ createIcon: function(size) {
+ return new St.Icon({ gicon: this.mount.get_icon(),
+ icon_size: size });
+ }
+});
+
+const AutorunTransientNotification = new Lang.Class({
+ Name: 'AutorunTransientNotification',
+ Extends: MessageTray.Notification,
+
+ _init: function(manager, source) {
+ this.parent(source, source.title, null, { customContent: true });
+
+ this._manager = manager;
+ this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box',
+ vertical: true });
+ this.addActor(this._box);
+
+ this._mount = source.mount;
+
+ source.apps.forEach(Lang.bind(this, function (app) {
+ let actor = this._buttonForApp(app);
+
+ if (actor)
+ this._box.add(actor, { x_fill: true,
+ x_align: St.Align.START });
+ }));
+
+ this._box.add(this._buttonForEject(), { x_fill: true,
+ x_align: St.Align.START });
+
+ // set the notification to transient and urgent, so that it
+ // expands out
+ this.setTransient(true);
+ this.setUrgency(MessageTray.Urgency.CRITICAL);
+ },
+
+ _buttonForApp: function(app) {
+ let box = new St.BoxLayout();
+ let icon = new St.Icon({ gicon: app.get_icon(),
+ style_class: 'hotplug-notification-item-icon' });
+ box.add(icon);
+
+ let label = new St.Bin({ y_align: St.Align.MIDDLE,
+ child: new St.Label
+ ({ text: _("Open with %s").format(app.get_name()) })
+ });
+ box.add(label);
+
+ let button = new St.Button({ child: box,
+ x_fill: true,
+ x_align: St.Align.START,
+ button_mask: St.ButtonMask.ONE,
+ style_class: 'hotplug-notification-item' });
+
+ button.connect('clicked', Lang.bind(this, function() {
+ startAppForMount(app, this._mount);
+ this.destroy();
+ }));
+
+ return button;
+ },
+
+ _buttonForEject: function() {
+ let box = new St.BoxLayout();
+ let icon = new St.Icon({ icon_name: 'media-eject',
+ style_class: 'hotplug-notification-item-icon' });
+ box.add(icon);
+
+ let label = new St.Bin({ y_align: St.Align.MIDDLE,
+ child: new St.Label
+ ({ text: _("Eject") })
+ });
+ box.add(label);
+
+ let button = new St.Button({ child: box,
+ x_fill: true,
+ x_align: St.Align.START,
+ button_mask: St.ButtonMask.ONE,
+ style_class: 'hotplug-notification-item' });
+
+ button.connect('clicked', Lang.bind(this, function() {
+ this._manager.ejectMount(this._mount);
+ }));
+
+ return button;
+ }
+});
+
+const Component = AutorunManager;
--- /dev/null
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Lang = imports.lang;
+const Shell = imports.gi.Shell;
+const Clutter = imports.gi.Clutter;
+const St = imports.gi.St;
+const Pango = imports.gi.Pango;
+const Gio = imports.gi.Gio;
+const GObject = imports.gi.GObject;
+const Gcr = imports.gi.Gcr;
+
+const ModalDialog = imports.ui.modalDialog;
+const ShellEntry = imports.ui.shellEntry;
+const CheckBox = imports.ui.checkBox;
+
+let prompter = null;
+
+const KeyringDialog = new Lang.Class({
+ Name: 'KeyringDialog',
+ Extends: ModalDialog.ModalDialog,
+
+ _init: function() {
+ this.parent({ styleClass: 'prompt-dialog' });
+
+ this.prompt = new Shell.KeyringPrompt();
+ this.prompt.connect('show-password', Lang.bind(this, this._onShowPassword));
+ this.prompt.connect('show-confirm', Lang.bind(this, this._onShowConfirm));
+ this.prompt.connect('hide-prompt', Lang.bind(this, this._onHidePrompt));
+
+ let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
+ vertical: false });
+ this.contentLayout.add(mainContentBox);
+
+ let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
+ mainContentBox.add(icon,
+ { x_fill: true,
+ y_fill: false,
+ x_align: St.Align.END,
+ y_align: St.Align.START });
+
+ this._messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
+ vertical: true });
+ mainContentBox.add(this._messageBox,
+ { y_align: St.Align.START, expand: true, x_fill: true, y_fill: true });
+
+ let subject = new St.Label({ style_class: 'prompt-dialog-headline' });
+ this.prompt.bind_property('message', subject, 'text', GObject.BindingFlags.SYNC_CREATE);
+
+ this._messageBox.add(subject,
+ { y_fill: false,
+ y_align: St.Align.START });
+
+ let description = new St.Label({ style_class: 'prompt-dialog-description' });
+ description.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ description.clutter_text.line_wrap = true;
+ this.prompt.bind_property('description', description, 'text', GObject.BindingFlags.SYNC_CREATE);
+ this._messageBox.add(description,
+ { y_fill: true,
+ y_align: St.Align.START });
+
+ this._controlTable = null;
+
+ let buttons = [{ label: '',
+ action: Lang.bind(this, this._onCancelButton),
+ key: Clutter.Escape
+ },
+ { label: '',
+ action: Lang.bind(this, this._onContinueButton),
+ default: true
+ }]
+
+ this.setButtons(buttons);
+ this._cancelButton = buttons[0].button;
+ this._continueButton = buttons[1].button;
+
+ this.prompt.bind_property('cancel-label', this._cancelButton, 'label', GObject.BindingFlags.SYNC_CREATE);
+ this.prompt.bind_property('continue-label', this._continueButton, 'label', GObject.BindingFlags.SYNC_CREATE);
+ },
+
+ _buildControlTable: function() {
+ let table = new St.Table({ style_class: 'keyring-dialog-control-table' });
+ let row = 0;
+
+ if (this.prompt.password_visible) {
+ let label = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
+ label.set_text(_("Password:"));
+ table.add(label, { row: row, col: 0,
+ x_expand: false, x_fill: true,
+ x_align: St.Align.START,
+ y_fill: false, y_align: St.Align.MIDDLE });
+ this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
+ text: '',
+ can_focus: true});
+ this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
+ ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true });
+ this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onPasswordActivate));
+ table.add(this._passwordEntry, { row: row, col: 1, x_expand: true, x_fill: true, x_align: St.Align.START });
+ row++;
+ } else {
+ this._passwordEntry = null;
+ }
+
+ if (this.prompt.confirm_visible) {
+ var label = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
+ label.set_text(_("Type again:"));
+ table.add(label, { row: row, col: 0,
+ x_expand: false, x_fill: true,
+ x_align: St.Align.START,
+ y_fill: false, y_align: St.Align.MIDDLE });
+ this._confirmEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
+ text: '',
+ can_focus: true});
+ this._confirmEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
+ ShellEntry.addContextMenu(this._confirmEntry, { isPassword: true });
+ this._confirmEntry.clutter_text.connect('activate', Lang.bind(this, this._onConfirmActivate));
+ table.add(this._confirmEntry, { row: row, col: 1, x_expand: true, x_fill: true, x_align: St.Align.START });
+ row++;
+ } else {
+ this._confirmEntry = null;
+ }
+
+ this.prompt.set_password_actor(this._passwordEntry ? this._passwordEntry.clutter_text : null);
+ this.prompt.set_confirm_actor(this._confirmEntry ? this._confirmEntry.clutter_text : null);
+
+ if (this.prompt.choice_visible) {
+ let choice = new CheckBox.CheckBox();
+ this.prompt.bind_property('choice-label', choice.getLabelActor(), 'text', GObject.BindingFlags.SYNC_CREATE);
+ this.prompt.bind_property('choice-chosen', choice.actor, 'checked', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);
+ table.add(choice.actor, { row: row, col: 1, x_expand: false, x_fill: true, x_align: St.Align.START });
+ row++;
+ }
+
+ let warning = new St.Label({ style_class: 'prompt-dialog-error-label' });
+ warning.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ warning.clutter_text.line_wrap = true;
+ table.add(warning, { row: row, col: 1, x_expand: false, x_fill: false, x_align: St.Align.START });
+ this.prompt.bind_property('warning-visible', warning, 'visible', GObject.BindingFlags.SYNC_CREATE);
+ this.prompt.bind_property('warning', warning, 'text', GObject.BindingFlags.SYNC_CREATE);
+
+ if (this._controlTable) {
+ this._controlTable.destroy_all_children();
+ this._controlTable.destroy();
+ }
+
+ this._controlTable = table;
+ this._messageBox.add(table, { x_fill: true, y_fill: true });
+ },
+
+ _ensureOpen: function() {
+ // NOTE: ModalDialog.open() is safe to call if the dialog is
+ // already open - it just returns true without side-effects
+ if (this.open())
+ return true;
+
+ // The above fail if e.g. unable to get input grab
+ //
+ // In an ideal world this wouldn't happen (because the
+ // Shell is in complete control of the session) but that's
+ // just not how things work right now.
+
+ log('keyringPrompt: Failed to show modal dialog.' +
+ ' Dismissing prompt request');
+ this.prompt.cancel()
+ return false;
+ },
+
+ _onShowPassword: function(prompt) {
+ this._buildControlTable();
+ this._ensureOpen();
+ this._passwordEntry.grab_key_focus();
+ },
+
+ _onShowConfirm: function(prompt) {
+ this._buildControlTable();
+ this._ensureOpen();
+ this._continueButton.grab_key_focus();
+ },
+
+ _onHidePrompt: function(prompt) {
+ this.close();
+ },
+
+ _onPasswordActivate: function() {
+ if (this.prompt.confirm_visible)
+ this._confirmEntry.grab_key_focus();
+ else
+ this._onContinueButton();
+ },
+
+ _onConfirmActivate: function() {
+ this._onContinueButton();
+ },
+
+ _onContinueButton: function() {
+ this.prompt.complete();
+ },
+
+ _onCancelButton: function() {
+ this.prompt.cancel();
+ },
+});
+
+const KeyringPrompter = new Lang.Class({
+ Name: 'KeyringPrompter',
+
+ _init: function() {
+ this._prompter = new Gcr.SystemPrompter();
+ this._prompter.connect('new-prompt', function(prompter) {
+ let dialog = new KeyringDialog();
+ return dialog.prompt;
+ });
+ this._dbusId = null;
+ },
+
+ enable: function() {
+ this._prompter.register(Gio.DBus.session);
+ this._dbusId = Gio.DBus.session.own_name('org.gnome.keyring.SystemPrompter',
+ Gio.BusNameOwnerFlags.REPLACE, null, null);
+ },
+
+ disable: function() {
+ Gio.DBus.session.unown_name(this._dbusId);
+ }
+});
+
+const Component = KeyringPrompter;
--- /dev/null
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Lang = imports.lang;
+const NetworkManager = imports.gi.NetworkManager;
+const NMClient = imports.gi.NMClient;
+const Pango = imports.gi.Pango;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+
+const Config = imports.misc.config;
+const ModalDialog = imports.ui.modalDialog;
+const PopupMenu = imports.ui.popupMenu;
+const ShellEntry = imports.ui.shellEntry;
+
+const VPN_UI_GROUP = 'VPN Plugin UI';
+
+const NetworkSecretDialog = new Lang.Class({
+ Name: 'NetworkSecretDialog',
+ Extends: ModalDialog.ModalDialog,
+
+ _init: function(agent, requestId, connection, settingName, hints, contentOverride) {
+ this.parent({ styleClass: 'prompt-dialog' });
+
+ this._agent = agent;
+ this._requestId = requestId;
+ this._connection = connection;
+ this._settingName = settingName;
+ this._hints = hints;
+
+ if (contentOverride)
+ this._content = contentOverride;
+ else
+ this._content = this._getContent();
+
+ let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
+ vertical: false });
+ this.contentLayout.add(mainContentBox,
+ { x_fill: true,
+ y_fill: true });
+
+ let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
+ mainContentBox.add(icon,
+ { x_fill: true,
+ y_fill: false,
+ x_align: St.Align.END,
+ y_align: St.Align.START });
+
+ let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
+ vertical: true });
+ mainContentBox.add(messageBox,
+ { y_align: St.Align.START });
+
+ let subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline',
+ text: this._content.title });
+ messageBox.add(subjectLabel,
+ { y_fill: false,
+ y_align: St.Align.START });
+
+ if (this._content.message != null) {
+ let descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description',
+ text: this._content.message,
+ // HACK: for reasons unknown to me, the label
+ // is not asked the correct height for width,
+ // and thus is underallocated
+ // place a fixed height to avoid overflowing
+ style: 'height: 3em'
+ });
+ descriptionLabel.clutter_text.line_wrap = true;
+
+ messageBox.add(descriptionLabel,
+ { y_fill: true,
+ y_align: St.Align.START,
+ expand: true });
+ }
+
+ let secretTable = new St.Table({ style_class: 'network-dialog-secret-table' });
+ let initialFocusSet = false;
+ let pos = 0;
+ for (let i = 0; i < this._content.secrets.length; i++) {
+ let secret = this._content.secrets[i];
+ let label = new St.Label({ style_class: 'prompt-dialog-password-label',
+ text: secret.label });
+
+ let reactive = secret.key != null;
+
+ secret.entry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
+ text: secret.value, can_focus: reactive,
+ reactive: reactive });
+ ShellEntry.addContextMenu(secret.entry,
+ { isPassword: secret.password });
+
+ if (secret.validate)
+ secret.valid = secret.validate(secret);
+ else // no special validation, just ensure it's not empty
+ secret.valid = secret.value.length > 0;
+
+ if (reactive) {
+ if (!initialFocusSet) {
+ this.setInitialKeyFocus(secret.entry);
+ initialFocusSet = true;
+ }
+
+ secret.entry.clutter_text.connect('activate', Lang.bind(this, this._onOk));
+ secret.entry.clutter_text.connect('text-changed', Lang.bind(this, function() {
+ secret.value = secret.entry.get_text();
+ if (secret.validate)
+ secret.valid = secret.validate(secret);
+ else
+ secret.valid = secret.value.length > 0;
+ this._updateOkButton();
+ }));
+ } else
+ secret.valid = true;
+
+ secretTable.add(label, { row: pos, col: 0,
+ x_expand: false, x_fill: true,
+ x_align: St.Align.START,
+ y_fill: false, y_align: St.Align.MIDDLE });
+ secretTable.add(secret.entry, { row: pos, col: 1, x_expand: true, x_fill: true, y_align: St.Align.END });
+ pos++;
+
+ if (secret.password)
+ secret.entry.clutter_text.set_password_char('\u25cf');
+ }
+
+ messageBox.add(secretTable);
+
+ this._okButton = { label: _("Connect"),
+ action: Lang.bind(this, this._onOk),
+ default: true
+ };
+
+ this.setButtons([{ label: _("Cancel"),
+ action: Lang.bind(this, this.cancel),
+ key: Clutter.KEY_Escape,
+ },
+ this._okButton]);
+ },
+
+ _updateOkButton: function() {
+ let valid = true;
+ for (let i = 0; i < this._content.secrets.length; i++) {
+ let secret = this._content.secrets[i];
+ valid = valid && secret.valid;
+ }
+
+ this._okButton.button.reactive = valid;
+ this._okButton.button.can_focus = valid;
+ },
+
+ _onOk: function() {
+ let valid = true;
+ for (let i = 0; i < this._content.secrets.length; i++) {
+ let secret = this._content.secrets[i];
+ valid = valid && secret.valid;
+ if (secret.key != null)
+ this._agent.set_password(this._requestId, secret.key, secret.value);
+ }
+
+ if (valid) {
+ this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
+ this.close(global.get_current_time());
+ }
+ // do nothing if not valid
+ },
+
+ cancel: function() {
+ this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
+ this.close(global.get_current_time());
+ },
+
+ _validateWpaPsk: function(secret) {
+ let value = secret.value;
+ if (value.length == 64) {
+ // must be composed of hexadecimal digits only
+ for (let i = 0; i < 64; i++) {
+ if (!((value[i] >= 'a' && value[i] <= 'f')
+ || (value[i] >= 'A' && value[i] <= 'F')
+ || (value[i] >= '0' && value[i] <= '9')))
+ return false;
+ }
+ return true;
+ }
+
+ return (value.length >= 8 && value.length <= 63);
+ },
+
+ _validateStaticWep: function(secret) {
+ let value = secret.value;
+ if (secret.wep_key_type == NetworkManager.WepKeyType.KEY) {
+ if (value.length == 10 || value.length == 26) {
+ for (let i = 0; i < value.length; i++) {
+ if (!((value[i] >= 'a' && value[i] <= 'f')
+ || (value[i] >= 'A' && value[i] <= 'F')
+ || (value[i] >= '0' && value[i] <= '9')))
+ return false;
+ }
+ } else if (value.length == 5 || value.length == 13) {
+ for (let i = 0; i < value.length; i++) {
+ if (!((value[i] >= 'a' && value[i] <= 'z')
+ || (value[i] >= 'A' && value[i] <= 'Z')))
+ return false;
+ }
+ } else
+ return false;
+ } else if (secret.wep_key_type == NetworkManager.WepKeyType.PASSPHRASE) {
+ if (value.length < 0 || value.length > 64)
+ return false;
+ }
+ return true;
+ },
+
+ _getWirelessSecrets: function(secrets, wirelessSetting) {
+ let wirelessSecuritySetting = this._connection.get_setting_wireless_security();
+ switch (wirelessSecuritySetting.key_mgmt) {
+ // First the easy ones
+ case 'wpa-none':
+ case 'wpa-psk':
+ secrets.push({ label: _("Password: "), key: 'psk',
+ value: wirelessSecuritySetting.psk || '',
+ validate: this._validateWpaPsk, password: true });
+ break;
+ case 'none': // static WEP
+ secrets.push({ label: _("Key: "), key: 'wep-key' + wirelessSecuritySetting.wep_tx_keyidx,
+ value: wirelessSecuritySetting.get_wep_key(wirelessSecuritySetting.wep_tx_keyidx) || '',
+ wep_key_type: wirelessSecuritySetting.wep_key_type,
+ validate: this._validateStaticWep, password: true });
+ break;
+ case 'ieee8021x':
+ if (wirelessSecuritySetting.auth_alg == 'leap') // Cisco LEAP
+ secrets.push({ label: _("Password: "), key: 'leap-password',
+ value: wirelessSecuritySetting.leap_password || '', password: true });
+ else // Dynamic (IEEE 802.1x) WEP
+ this._get8021xSecrets(secrets);
+ break;
+ case 'wpa-eap':
+ this._get8021xSecrets(secrets);
+ break;
+ default:
+ log('Invalid wireless key management: ' + wirelessSecuritySetting.key_mgmt);
+ }
+ },
+
+ _get8021xSecrets: function(secrets) {
+ let ieee8021xSetting = this._connection.get_setting_802_1x();
+ let phase2method;
+
+ switch (ieee8021xSetting.get_eap_method(0)) {
+ case 'md5':
+ case 'leap':
+ case 'ttls':
+ case 'peap':
+ // TTLS and PEAP are actually much more complicated, but this complication
+ // is not visible here since we only care about phase2 authentication
+ // (and don't even care of which one)
+ secrets.push({ label: _("Username: "), key: null,
+ value: ieee8021xSetting.identity || '', password: false });
+ secrets.push({ label: _("Password: "), key: 'password',
+ value: ieee8021xSetting.password || '', password: true });
+ break;
+ case 'tls':
+ secrets.push({ label: _("Identity: "), key: null,
+ value: ieee8021xSetting.identity || '', password: false });
+ secrets.push({ label: _("Private key password: "), key: 'private-key-password',
+ value: ieee8021xSetting.private_key_password || '', password: true });
+ break;
+ default:
+ log('Invalid EAP/IEEE802.1x method: ' + ieee8021xSetting.get_eap_method(0));
+ }
+ },
+
+ _getPPPoESecrets: function(secrets) {
+ let pppoeSetting = this._connection.get_setting_pppoe();
+ secrets.push({ label: _("Username: "), key: 'username',
+ value: pppoeSetting.username || '', password: false });
+ secrets.push({ label: _("Service: "), key: 'service',
+ value: pppoeSetting.service || '', password: false });
+ secrets.push({ label: _("Password: "), key: 'password',
+ value: pppoeSetting.password || '', password: true });
+ },
+
+ _getMobileSecrets: function(secrets, connectionType) {
+ let setting;
+ if (connectionType == 'bluetooth')
+ setting = this._connection.get_setting_cdma() || this._connection.get_setting_gsm();
+ else
+ setting = this._connection.get_setting_by_name(connectionType);
+ secrets.push({ label: _("Password: "), key: 'password',
+ value: setting.value || '', password: true });
+ },
+
+ _getContent: function() {
+ let connectionSetting = this._connection.get_setting_connection();
+ let connectionType = connectionSetting.get_connection_type();
+ let wirelessSetting;
+ let ssid;
+
+ let content = { };
+ content.secrets = [ ];
+
+ switch (connectionType) {
+ case '802-11-wireless':
+ wirelessSetting = this._connection.get_setting_wireless();
+ ssid = NetworkManager.utils_ssid_to_utf8(wirelessSetting.get_ssid());
+ content.title = _("Authentication required by wireless network");
+ content.message = _("Passwords or encryption keys are required to access the wireless network '%s'.").format(ssid);
+ this._getWirelessSecrets(content.secrets, wirelessSetting);
+ break;
+ case '802-3-ethernet':
+ content.title = _("Wired 802.1X authentication");
+ content.message = null;
+ content.secrets.push({ label: _("Network name: "), key: null,
+ value: connectionSetting.get_id(), password: false });
+ this._get8021xSecrets(content.secrets);
+ break;
+ case 'pppoe':
+ content.title = _("DSL authentication");
+ content.message = null;
+ this._getPPPoESecrets(content.secrets);
+ break;
+ case 'gsm':
+ if (this._hints.indexOf('pin') != -1) {
+ let gsmSetting = this._connection.get_setting_gsm();
+ content.title = _("PIN code required");
+ content.message = _("PIN code is needed for the mobile broadband device");
+ content.secrets.push({ label: _("PIN: "), key: 'pin',
+ value: gsmSetting.pin || '', password: true });
+ }
+ // fall through
+ case 'cdma':
+ case 'bluetooth':
+ content.title = _("Mobile broadband network password");
+ content.message = _("A password is required to connect to '%s'.").format(connectionSetting.get_id());
+ this._getMobileSecrets(content.secrets, connectionType);
+ break;
+ default:
+ log('Invalid connection type: ' + connectionType);
+ };
+
+ return content;
+ }
+});
+
+const VPNRequestHandler = new Lang.Class({
+ Name: 'VPNRequestHandler',
+
+ _init: function(agent, requestId, authHelper, serviceType, connection, hints, flags) {
+ this._agent = agent;
+ this._requestId = requestId;
+ this._connection = connection;
+ this._pluginOutBuffer = [];
+ this._title = null;
+ this._description = null;
+ this._content = [ ];
+ this._shellDialog = null;
+
+ let connectionSetting = connection.get_setting_connection();
+
+ let argv = [ authHelper.fileName,
+ '-u', connectionSetting.uuid,
+ '-n', connectionSetting.id,
+ '-s', serviceType
+ ];
+ if (authHelper.externalUIMode)
+ argv.push('--external-ui-mode');
+ if (flags & NMClient.SecretAgentGetSecretsFlags.ALLOW_INTERACTION)
+ argv.push('-i');
+ if (flags & NMClient.SecretAgentGetSecretsFlags.REQUEST_NEW)
+ argv.push('-r');
+
+ this._newStylePlugin = authHelper.externalUIMode;
+
+ try {
+ let [success, pid, stdin, stdout, stderr] =
+ GLib.spawn_async_with_pipes(null, /* pwd */
+ argv,
+ null, /* envp */
+ GLib.SpawnFlags.DO_NOT_REAP_CHILD,
+ null /* child_setup */);
+
+ this._childPid = pid;
+ this._stdin = new Gio.UnixOutputStream({ fd: stdin, close_fd: true });
+ this._stdout = new Gio.UnixInputStream({ fd: stdout, close_fd: true });
+ // We need this one too, even if don't actually care of what the process
+ // has to say on stderr, because otherwise the fd opened by g_spawn_async_with_pipes
+ // is kept open indefinitely
+ let stderrStream = new Gio.UnixInputStream({ fd: stderr, close_fd: true });
+ stderrStream.close(null);
+ this._dataStdout = new Gio.DataInputStream({ base_stream: this._stdout });
+
+ if (this._newStylePlugin)
+ this._readStdoutNewStyle();
+ else
+ this._readStdoutOldStyle();
+
+ this._childWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid,
+ Lang.bind(this, this._vpnChildFinished));
+
+ this._writeConnection();
+ } catch(e) {
+ logError(e, 'error while spawning VPN auth helper');
+
+ this._agent.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+ }
+ },
+
+ cancel: function() {
+ if (this._newStylePlugin && this._shellDialog) {
+ this._shellDialog.close(global.get_current_time());
+ this._shellDialog.destroy();
+ } else {
+ try {
+ this._stdin.write('QUIT\n\n', null);
+ } catch(e) { /* ignore broken pipe errors */ }
+ }
+
+ this.destroy();
+ },
+
+ destroy: function() {
+ if (this._destroyed)
+ return;
+
+ GLib.source_remove(this._childWatch);
+
+ this._stdin.close(null);
+ // Stdout is closed when we finish reading from it
+
+ this._destroyed = true;
+ },
+
+ _vpnChildFinished: function(pid, status, requestObj) {
+ if (this._newStylePlugin) {
+ // For new style plugin, all work is done in the async reading functions
+ // Just reap the process here
+ return;
+ }
+
+ let [exited, exitStatus] = Shell.util_wifexited(status);
+
+ if (exited) {
+ if (exitStatus != 0)
+ this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
+ else
+ this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
+ } else
+ this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+
+ this.destroy();
+ },
+
+ _vpnChildProcessLineOldStyle: function(line) {
+ if (this._previousLine != undefined) {
+ // Two consecutive newlines mean that the child should be closed
+ // (the actual newlines are eaten by Gio.DataInputStream)
+ // Send a termination message
+ if (line == '' && this._previousLine == '') {
+ try {
+ this._stdin.write('QUIT\n\n', null);
+ } catch(e) { /* ignore broken pipe errors */ }
+ } else {
+ this._agent.set_password(this._requestId, this._previousLine, line);
+ this._previousLine = undefined;
+ }
+ } else {
+ this._previousLine = line;
+ }
+ },
+
+ _readStdoutOldStyle: function() {
+ this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
+ let [line, len] = this._dataStdout.read_line_finish_utf8(result);
+
+ if (line == null) {
+ // end of file
+ this._stdout.close(null);
+ return;
+ }
+
+ this._vpnChildProcessLineOldStyle(line);
+
+ // try to read more!
+ this._readStdoutOldStyle();
+ }));
+ },
+
+ _readStdoutNewStyle: function() {
+ this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
+ let cnt = this._dataStdout.fill_finish(result);
+
+ if (cnt == 0) {
+ // end of file
+ this._showNewStyleDialog();
+
+ this._stdout.close(null);
+ return;
+ }
+
+ // Try to read more
+ this._dataStdout.set_buffer_size(2 * this._dataStdout.get_buffer_size());
+ this._readStdoutNewStyle();
+ }));
+ },
+
+ _showNewStyleDialog: function() {
+ let keyfile = new GLib.KeyFile();
+ let contentOverride;
+
+ try {
+ let data = this._dataStdout.peek_buffer();
+ keyfile.load_from_data(data.toString(), data.length,
+ GLib.KeyFileFlags.NONE);
+
+ if (keyfile.get_integer(VPN_UI_GROUP, 'Version') != 2)
+ throw new Error('Invalid plugin keyfile version, is %d');
+
+ contentOverride = { title: keyfile.get_string(VPN_UI_GROUP, 'Title'),
+ message: keyfile.get_string(VPN_UI_GROUP, 'Description'),
+ secrets: [] };
+
+ let [groups, len] = keyfile.get_groups();
+ for (let i = 0; i < groups.length; i++) {
+ if (groups[i] == VPN_UI_GROUP)
+ continue;
+
+ let value = keyfile.get_string(groups[i], 'Value');
+ let shouldAsk = keyfile.get_boolean(groups[i], 'ShouldAsk');
+
+ if (shouldAsk) {
+ contentOverride.secrets.push({ label: keyfile.get_string(groups[i], 'Label'),
+ key: groups[i],
+ value: value,
+ password: keyfile.get_boolean(groups[i], 'IsSecret')
+ });
+ } else {
+ if (!value.length) // Ignore empty secrets
+ continue;
+
+ this._agent.set_password(this._requestId, groups[i], value);
+ }
+ }
+ } catch(e) {
+ logError(e, 'error while reading VPN plugin output keyfile');
+
+ this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+ return;
+ }
+
+ if (contentOverride.secrets.length) {
+ // Only show the dialog if we actually have something to ask
+ this._shellDialog = new NetworkSecretDialog(this._agent, this._requestId, this._connection, 'vpn', [], contentOverride);
+ this._shellDialog.open(global.get_current_time());
+ } else {
+ this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
+ }
+ },
+
+ _writeConnection: function() {
+ let vpnSetting = this._connection.get_setting_vpn();
+
+ try {
+ vpnSetting.foreach_data_item(Lang.bind(this, function(key, value) {
+ this._stdin.write('DATA_KEY=' + key + '\n', null);
+ this._stdin.write('DATA_VAL=' + (value || '') + '\n\n', null);
+ }));
+ vpnSetting.foreach_secret(Lang.bind(this, function(key, value) {
+ this._stdin.write('SECRET_KEY=' + key + '\n', null);
+ this._stdin.write('SECRET_VAL=' + (value || '') + '\n\n', null);
+ }));
+ this._stdin.write('DONE\n\n', null);
+ } catch(e) {
+ logError(e, 'internal error while writing connection to helper');
+
+ this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+ }
+ },
+});
+
+const NetworkAgent = new Lang.Class({
+ Name: 'NetworkAgent',
+
+ _init: function() {
+ this._native = new Shell.NetworkAgent({ auto_register: false,
+ identifier: 'org.gnome.Shell.NetworkAgent' });
+
+ this._dialogs = { };
+ this._vpnRequests = { };
+
+ this._native.connect('new-request', Lang.bind(this, this._newRequest));
+ this._native.connect('cancel-request', Lang.bind(this, this._cancelRequest));
+ },
+
+ enable: function() {
+ this._native.register();
+ },
+
+ disable: function() {
+ this._native.unregister();
+ },
+
+ _newRequest: function(agent, requestId, connection, settingName, hints, flags) {
+ if (settingName == 'vpn') {
+ this._vpnRequest(requestId, connection, hints, flags);
+ return;
+ }
+
+ let dialog = new NetworkSecretDialog(agent, requestId, connection, settingName, hints);
+ dialog.connect('destroy', Lang.bind(this, function() {
+ delete this._dialogs[requestId];
+ }));
+ this._dialogs[requestId] = dialog;
+ dialog.open(global.get_current_time());
+ },
+
+ _cancelRequest: function(agent, requestId) {
+ if (this._dialogs[requestId]) {
+ this._dialogs[requestId].close(global.get_current_time());
+ this._dialogs[requestId].destroy();
+ delete this._dialogs[requestId];
+ } else if (this._vpnRequests[requestId]) {
+ this._vpnRequests[requestId].cancel();
+ delete this._vpnRequests[requestId];
+ }
+ },
+
+ _vpnRequest: function(requestId, connection, hints, flags) {
+ let vpnSetting = connection.get_setting_vpn();
+ let serviceType = vpnSetting.service_type;
+
+ this._buildVPNServiceCache();
+
+ let binary = this._vpnBinaries[serviceType];
+ if (!binary) {
+ log('Invalid VPN service type (cannot find authentication binary)');
+
+ /* cancel the auth process */
+ this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+ return;
+ }
+
+ this._vpnRequests[requestId] = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags);
+ },
+
+ _buildVPNServiceCache: function() {
+ if (this._vpnCacheBuilt)
+ return;
+
+ this._vpnCacheBuilt = true;
+ this._vpnBinaries = { };
+
+ let dir = Gio.file_new_for_path(GLib.build_filenamev([Config.SYSCONFDIR, 'NetworkManager/VPN']));
+ try {
+ let fileEnum = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
+ let info;
+
+ while ((info = fileEnum.next_file(null))) {
+ let name = info.get_name();
+ if (name.substr(-5) != '.name')
+ continue;
+
+ try {
+ let keyfile = new GLib.KeyFile();
+ keyfile.load_from_file(dir.get_child(name).get_path(), GLib.KeyFileFlags.NONE);
+ let service = keyfile.get_string('VPN Connection', 'service');
+ let binary = keyfile.get_string('GNOME', 'auth-dialog');
+ let externalUIMode = false;
+ try {
+ externalUIMode = keyfile.get_boolean('GNOME', 'supports-external-ui-mode');
+ } catch(e) { } // ignore errors if key does not exist
+ let path = binary;
+ if (!GLib.path_is_absolute(path)) {
+ path = GLib.build_filenamev([Config.LIBEXECDIR, path]);
+ }
+
+ if (GLib.file_test(path, GLib.FileTest.IS_EXECUTABLE))
+ this._vpnBinaries[service] = { fileName: path, externalUIMode: externalUIMode };
+ else
+ throw new Error('VPN plugin at %s is not executable'.format(path));
+ } catch(e) {
+ log('Error \'%s\' while processing VPN keyfile \'%s\''.
+ format(e.message, dir.get_child(name).get_path()));
+ continue;
+ }
+ }
+ } catch(e) {
+ logError(e, 'error while enumerating VPN auth helpers');
+ }
+ }
+});
+const Component = NetworkAgent;
--- /dev/null
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Lang = imports.lang;
+const Signals = imports.signals;
+const Shell = imports.gi.Shell;
+const AccountsService = imports.gi.AccountsService;
+const Clutter = imports.gi.Clutter;
+const St = imports.gi.St;
+const Pango = imports.gi.Pango;
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Mainloop = imports.mainloop;
+const Polkit = imports.gi.Polkit;
+const PolkitAgent = imports.gi.PolkitAgent;
+
+const Components = imports.ui.components;
+const ModalDialog = imports.ui.modalDialog;
+const ShellEntry = imports.ui.shellEntry;
+const UserMenu = imports.ui.userMenu;
+
+const DIALOG_ICON_SIZE = 48;
+
+const AuthenticationDialog = new Lang.Class({
+ Name: 'AuthenticationDialog',
+ Extends: ModalDialog.ModalDialog,
+
+ _init: function(actionId, message, cookie, userNames) {
+ this.parent({ styleClass: 'prompt-dialog' });
+
+ this.actionId = actionId;
+ this.message = message;
+ this.userNames = userNames;
+ this._wasDismissed = false;
+ this._completed = false;
+
+ let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
+ vertical: false });
+ this.contentLayout.add(mainContentBox,
+ { x_fill: true,
+ y_fill: true });
+
+ let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
+ mainContentBox.add(icon,
+ { x_fill: true,
+ y_fill: false,
+ x_align: St.Align.END,
+ y_align: St.Align.START });
+
+ let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
+ vertical: true });
+ mainContentBox.add(messageBox,
+ { y_align: St.Align.START });
+
+ this._subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline',
+ text: _("Authentication Required") });
+
+ messageBox.add(this._subjectLabel,
+ { y_fill: false,
+ y_align: St.Align.START });
+
+ this._descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description',
+ text: message });
+ this._descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this._descriptionLabel.clutter_text.line_wrap = true;
+
+ messageBox.add(this._descriptionLabel,
+ { y_fill: true,
+ y_align: St.Align.START });
+
+ if (userNames.length > 1) {
+ log('polkitAuthenticationAgent: Received ' + userNames.length +
+ ' identities that can be used for authentication. Only ' +
+ 'considering one.');
+ }
+
+ let userName = GLib.get_user_name();
+ if (userNames.indexOf(userName) < 0)
+ userName = 'root';
+ if (userNames.indexOf(userName) < 0)
+ userName = userNames[0];
+
+ this._user = AccountsService.UserManager.get_default().get_user(userName);
+ let userRealName = this._user.get_real_name()
+ this._userLoadedId = this._user.connect('notify::is_loaded',
+ Lang.bind(this, this._onUserChanged));
+ this._userChangedId = this._user.connect('changed',
+ Lang.bind(this, this._onUserChanged));
+
+ // Special case 'root'
+ let userIsRoot = false;
+ if (userName == 'root') {
+ userIsRoot = true;
+ userRealName = _("Administrator");
+ }
+
+ if (userIsRoot) {
+ let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-root-label',
+ text: userRealName }));
+ messageBox.add(userLabel);
+ } else {
+ let userBox = new St.BoxLayout({ style_class: 'polkit-dialog-user-layout',
+ vertical: false });
+ messageBox.add(userBox);
+ this._userAvatar = new UserMenu.UserAvatarWidget(this._user,
+ { iconSize: DIALOG_ICON_SIZE,
+ styleClass: 'polkit-dialog-user-icon' });
+ this._userAvatar.actor.hide();
+ userBox.add(this._userAvatar.actor,
+ { x_fill: true,
+ y_fill: false,
+ x_align: St.Align.END,
+ y_align: St.Align.START });
+ let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-label',
+ text: userRealName }));
+ userBox.add(userLabel,
+ { x_fill: true,
+ y_fill: false,
+ x_align: St.Align.END,
+ y_align: St.Align.MIDDLE });
+ }
+
+ this._onUserChanged();
+
+ this._passwordBox = new St.BoxLayout({ vertical: false, style_class: 'prompt-dialog-password-box' });
+ messageBox.add(this._passwordBox);
+ this._passwordLabel = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
+ this._passwordBox.add(this._passwordLabel, { y_fill: false, y_align: St.Align.MIDDLE });
+ this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
+ text: "",
+ can_focus: true});
+ ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true });
+ this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivate));
+ this._passwordBox.add(this._passwordEntry,
+ { expand: true });
+ this.setInitialKeyFocus(this._passwordEntry);
+ this._passwordBox.hide();
+
+ this._errorMessageLabel = new St.Label({ style_class: 'prompt-dialog-error-label' });
+ this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this._errorMessageLabel.clutter_text.line_wrap = true;
+ messageBox.add(this._errorMessageLabel);
+ this._errorMessageLabel.hide();
+
+ this._infoMessageLabel = new St.Label({ style_class: 'prompt-dialog-info-label' });
+ this._infoMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this._infoMessageLabel.clutter_text.line_wrap = true;
+ messageBox.add(this._infoMessageLabel);
+ this._infoMessageLabel.hide();
+
+ /* text is intentionally non-blank otherwise the height is not the same as for
+ * infoMessage and errorMessageLabel - but it is still invisible because
+ * gnome-shell.css sets the color to be transparent
+ */
+ this._nullMessageLabel = new St.Label({ style_class: 'prompt-dialog-null-label',
+ text: 'abc'});
+ this._nullMessageLabel.add_style_class_name('hidden');
+ this._nullMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this._nullMessageLabel.clutter_text.line_wrap = true;
+ messageBox.add(this._nullMessageLabel);
+ this._nullMessageLabel.show();
+
+ this.setButtons([{ label: _("Cancel"),
+ action: Lang.bind(this, this.cancel),
+ key: Clutter.Escape
+ },
+ { label: _("Authenticate"),
+ action: Lang.bind(this, this._onAuthenticateButtonPressed),
+ default: true
+ }]);
+
+ this._doneEmitted = false;
+
+ this._identityToAuth = Polkit.UnixUser.new_for_name(userName);
+ this._cookie = cookie;
+
+ this._session = new PolkitAgent.Session({ identity: this._identityToAuth,
+ cookie: this._cookie });
+ this._session.connect('completed', Lang.bind(this, this._onSessionCompleted));
+ this._session.connect('request', Lang.bind(this, this._onSessionRequest));
+ this._session.connect('show-error', Lang.bind(this, this._onSessionShowError));
+ this._session.connect('show-info', Lang.bind(this, this._onSessionShowInfo));
+ },
+
+ startAuthentication: function() {
+ this._session.initiate();
+ },
+
+ _ensureOpen: function() {
+ // NOTE: ModalDialog.open() is safe to call if the dialog is
+ // already open - it just returns true without side-effects
+ if (!this.open(global.get_current_time())) {
+ // This can fail if e.g. unable to get input grab
+ //
+ // In an ideal world this wouldn't happen (because the
+ // Shell is in complete control of the session) but that's
+ // just not how things work right now.
+ //
+ // One way to make this happen is by running 'sleep 3;
+ // pkexec bash' and then opening a popup menu.
+ //
+ // We could add retrying if this turns out to be a problem
+
+ log('polkitAuthenticationAgent: Failed to show modal dialog.' +
+ ' Dismissing authentication request for action-id ' + this.actionId +
+ ' cookie ' + this._cookie);
+ this._emitDone(false, true);
+ }
+ },
+
+ _emitDone: function(keepVisible, dismissed) {
+ if (!this._doneEmitted) {
+ this._doneEmitted = true;
+ this.emit('done', keepVisible, dismissed);
+ }
+ },
+
+ _onEntryActivate: function() {
+ let response = this._passwordEntry.get_text();
+ this._session.response(response);
+ // When the user responds, dismiss already shown info and
+ // error texts (if any)
+ this._errorMessageLabel.hide();
+ this._infoMessageLabel.hide();
+ this._nullMessageLabel.show();
+ },
+
+ _onAuthenticateButtonPressed: function() {
+ this._onEntryActivate();
+ },
+
+ _onSessionCompleted: function(session, gainedAuthorization) {
+ if (this._completed)
+ return;
+
+ this._completed = true;
+
+ if (!gainedAuthorization) {
+ /* Unless we are showing an existing error message from the PAM
+ * module (the PAM module could be reporting the authentication
+ * error providing authentication-method specific information),
+ * show "Sorry, that didn't work. Please try again."
+ */
+ if (!this._errorMessageLabel.visible && !this._wasDismissed) {
+ /* Translators: "that didn't work" refers to the fact that the
+ * requested authentication was not gained; this can happen
+ * because of an authentication error (like invalid password),
+ * for instance. */
+ this._errorMessageLabel.set_text(_("Sorry, that didn\'t work. Please try again."));
+ this._errorMessageLabel.show();
+ this._infoMessageLabel.hide();
+ this._nullMessageLabel.hide();
+ }
+ }
+ this._emitDone(!gainedAuthorization, false);
+ },
+
+ _onSessionRequest: function(session, request, echo_on) {
+ // Cheap localization trick
+ if (request == 'Password:' || request == 'Password: ')
+ this._passwordLabel.set_text(_("Password:"));
+ else
+ this._passwordLabel.set_text(request);
+
+ if (echo_on)
+ this._passwordEntry.clutter_text.set_password_char('');
+ else
+ this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
+
+ this._passwordBox.show();
+ this._passwordEntry.set_text('');
+ this._passwordEntry.grab_key_focus();
+ this._ensureOpen();
+ },
+
+ _onSessionShowError: function(session, text) {
+ this._passwordEntry.set_text('');
+ this._errorMessageLabel.set_text(text);
+ this._errorMessageLabel.show();
+ this._infoMessageLabel.hide();
+ this._nullMessageLabel.hide();
+ this._ensureOpen();
+ },
+
+ _onSessionShowInfo: function(session, text) {
+ this._passwordEntry.set_text('');
+ this._infoMessageLabel.set_text(text);
+ this._infoMessageLabel.show();
+ this._errorMessageLabel.hide();
+ this._nullMessageLabel.hide();
+ this._ensureOpen();
+ },
+
+ destroySession: function() {
+ if (this._session) {
+ if (!this._completed)
+ this._session.cancel();
+ this._session = null;
+ }
+ },
+
+ _onUserChanged: function() {
+ if (this._user.is_loaded) {
+ this._userAvatar.update();
+ this._userAvatar.actor.show();
+ }
+ },
+
+ cancel: function() {
+ this._wasDismissed = true;
+ this.close(global.get_current_time());
+ this._emitDone(false, true);
+ },
+});
+Signals.addSignalMethods(AuthenticationDialog.prototype);
+
+const AuthenticationAgent = new Lang.Class({
+ Name: 'AuthenticationAgent',
+
+ _init: function() {
+ this._currentDialog = null;
+ this._isCompleting = false;
+ this._handle = null;
+ this._native = new Shell.PolkitAuthenticationAgent();
+ this._native.connect('initiate', Lang.bind(this, this._onInitiate));
+ this._native.connect('cancel', Lang.bind(this, this._onCancel));
+ },
+
+ enable: function() {
+ this._native.register();
+ },
+
+ disable: function() {
+ this._native.unregister();
+ },
+
+ _onInitiate: function(nativeAgent, actionId, message, iconName, cookie, userNames) {
+ this._currentDialog = new AuthenticationDialog(actionId, message, cookie, userNames);
+
+ // We actually don't want to open the dialog until we know for
+ // sure that we're going to interact with the user. For
+ // example, if the password for the identity to auth is blank
+ // (which it will be on a live CD) then there will be no
+ // conversation at all... of course, we don't *know* that
+ // until we actually try it.
+ //
+ // See https://bugzilla.gnome.org/show_bug.cgi?id=643062 for more
+ // discussion.
+
+ this._currentDialog.connect('done', Lang.bind(this, this._onDialogDone));
+ this._currentDialog.startAuthentication();
+ },
+
+ _onCancel: function(nativeAgent) {
+ this._completeRequest(false, false);
+ },
+
+ _onDialogDone: function(dialog, keepVisible, dismissed) {
+ this._completeRequest(keepVisible, dismissed);
+ },
+
+ _reallyCompleteRequest: function(dismissed) {
+ this._currentDialog.close();
+ this._currentDialog.destroySession();
+ this._currentDialog = null;
+ this._isCompleting = false;
+
+ this._native.complete(dismissed)
+ },
+
+ _completeRequest: function(keepVisible, wasDismissed) {
+ if (this._isCompleting)
+ return;
+
+ this._isCompleting = true;
+
+ if (keepVisible) {
+ // Give the user 2 seconds to read 'Authentication Failure' before
+ // dismissing the dialog
+ Mainloop.timeout_add(2000,
+ Lang.bind(this,
+ function() {
+ this._reallyCompleteRequest(wasDismissed);
+ return false;
+ }));
+ } else {
+ this._reallyCompleteRequest(wasDismissed);
+ }
+ }
+});
+
+const Component = AuthenticationAgent;
--- /dev/null
+
+const Lang = imports.lang;
+
+const Gio = imports.gi.Gio;
+const Meta = imports.gi.Meta;
+const Shell = imports.gi.Shell;
+
+const Recorder = new Lang.Class({
+ Name: 'Recorder',
+
+ _init: function() {
+ this._recorderSettings = new Gio.Settings({ schema: 'org.gnome.shell.recorder' });
+ this._desktopLockdownSettings = new Gio.Settings({ schema: 'org.gnome.desktop.lockdown' });
+ this._bindingSettings = new Gio.Settings({ schema: 'org.gnome.shell.keybindings' });
+ this._recorder = null;
+ },
+
+ enable: function() {
+ global.display.add_keybinding('toggle-recording',
+ this._bindingSettings,
+ Meta.KeyBindingFlags.NONE, Lang.bind(this, this._toggleRecorder));
+ },
+
+ disable: function() {
+ global.display.remove_keybinding('toggle-recording');
+ },
+
+ _ensureRecorder: function() {
+ if (this._recorder == null)
+ this._recorder = new Shell.Recorder({ stage: global.stage });
+ return this._recorder;
+ },
+
+ _toggleRecorder: function() {
+ let recorder = this._ensureRecorder();
+ if (recorder.is_recording()) {
+ recorder.close();
+ Meta.enable_unredirect_for_screen(global.screen);
+ } else if (!desktopLockdownSettings.get_boolean('disable-save-to-disk')) {
+ // read the parameters from GSettings always in case they have changed
+ recorder.set_framerate(recorderSettings.get_int('framerate'));
+ /* Translators: this is a filename used for screencast recording */
+ // xgettext:no-c-format
+ recorder.set_filename(_("Screencast from %d %t") + '.' + recorderSettings.get_string('file-extension'));
+ let pipeline = recorderSettings.get_string('pipeline');
+
+ if (!pipeline.match(/^\s*$/))
+ recorder.set_pipeline(pipeline);
+ else
+ recorder.set_pipeline(null);
+
+ Meta.disable_unredirect_for_screen(global.screen);
+ recorder.record();
+ }
+ }
+});
+
+const Component = Recorder;
--- /dev/null
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Shell = imports.gi.Shell;
+const Signals = imports.signals;
+const St = imports.gi.St;
+const Tpl = imports.gi.TelepathyLogger;
+const Tp = imports.gi.TelepathyGLib;
+
+const History = imports.misc.history;
+const Params = imports.misc.params;
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+
+
+// See Notification.appendMessage
+const SCROLLBACK_IMMEDIATE_TIME = 60; // 1 minute
+const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
+const SCROLLBACK_RECENT_LENGTH = 20;
+const SCROLLBACK_IDLE_LENGTH = 5;
+
+// See Source._displayPendingMessages
+const SCROLLBACK_HISTORY_LINES = 10;
+
+// See Notification._onEntryChanged
+const COMPOSING_STOP_TIMEOUT = 5;
+
+const NotificationDirection = {
+ SENT: 'chat-sent',
+ RECEIVED: 'chat-received'
+};
+
+function makeMessageFromTpMessage(tpMessage, direction) {
+ let [text, flags] = tpMessage.to_text();
+
+ let timestamp = tpMessage.get_sent_timestamp();
+ if (timestamp == 0)
+ timestamp = tpMessage.get_received_timestamp();
+
+ return {
+ messageType: tpMessage.get_message_type(),
+ text: text,
+ sender: tpMessage.sender.alias,
+ timestamp: timestamp,
+ direction: direction
+ };
+}
+
+
+function makeMessageFromTplEvent(event) {
+ let sent = event.get_sender().get_entity_type() == Tpl.EntityType.SELF;
+ let direction = sent ? NotificationDirection.SENT : NotificationDirection.RECEIVED;
+
+ return {
+ messageType: event.get_message_type(),
+ text: event.get_message(),
+ sender: event.get_sender().get_alias(),
+ timestamp: event.get_timestamp(),
+ direction: direction
+ };
+}
+
+const TelepathyClient = new Lang.Class({
+ Name: 'TelepathyClient',
+
+ _init: function() {
+ // channel path -> ChatSource
+ this._chatSources = {};
+ this._chatState = Tp.ChannelChatState.ACTIVE;
+
+ // account path -> AccountNotification
+ this._accountNotifications = {};
+
+ // Define features we want
+ this._accountManager = Tp.AccountManager.dup();
+ let factory = this._accountManager.get_factory();
+ factory.add_account_features([Tp.Account.get_feature_quark_connection()]);
+ factory.add_connection_features([Tp.Connection.get_feature_quark_contact_list()]);
+ factory.add_channel_features([Tp.Channel.get_feature_quark_contacts()]);
+ factory.add_contact_features([Tp.ContactFeature.ALIAS,
+ Tp.ContactFeature.AVATAR_DATA,
+ Tp.ContactFeature.PRESENCE,
+ Tp.ContactFeature.SUBSCRIPTION_STATES]);
+
+ // Set up a SimpleObserver, which will call _observeChannels whenever a
+ // channel matching its filters is detected.
+ // The second argument, recover, means _observeChannels will be run
+ // for any existing channel as well.
+ this._tpClient = new Shell.TpClient({ name: 'GnomeShell',
+ account_manager: this._accountManager,
+ uniquify_name: true });
+ this._tpClient.set_observe_channels_func(
+ Lang.bind(this, this._observeChannels));
+ this._tpClient.set_approve_channels_func(
+ Lang.bind(this, this._approveChannels));
+ this._tpClient.set_handle_channels_func(
+ Lang.bind(this, this._handleChannels));
+
+ // Watch subscription requests and connection errors
+ this._subscriptionSource = null;
+ this._accountSource = null;
+
+ // Workaround for gjs not supporting GPtrArray in signals.
+ // See BGO bug #653941 for context.
+ this._tpClient.set_contact_list_changed_func(
+ Lang.bind(this, this._contactListChanged));
+
+ // Allow other clients (such as Empathy) to pre-empt our channels if
+ // needed
+ this._tpClient.set_delegated_channels_callback(
+ Lang.bind(this, this._delegatedChannelsCb));
+ },
+
+ enable: function() {
+ try {
+ this._tpClient.register();
+ } catch (e) {
+ throw new Error('Couldn\'t register Telepathy client. Error: \n' + e);
+ }
+
+ this._accountManagerValidityChangedId = this._accountManager.connect('account-validity-changed',
+ Lang.bind(this, this._accountValidityChanged));
+
+ if (!this._accountManager.is_prepared(Tp.AccountManager.get_feature_quark_core()))
+ this._accountManager.prepare_async(null, Lang.bind(this, this._accountManagerPrepared));
+ },
+
+ disable: function() {
+ this._tpClient.unregister();
+ this._accountManager.disconnect(this._accountManagerValidityChangedId);
+ this._accountManagerValidityChangedId = 0;
+ },
+
+ _observeChannels: function(observer, account, conn, channels,
+ dispatchOp, requests, context) {
+ let len = channels.length;
+ for (let i = 0; i < len; i++) {
+ let channel = channels[i];
+ let [targetHandle, targetHandleType] = channel.get_handle();
+
+ if (channel.get_invalidated())
+ continue;
+
+ /* Only observe contact text channels */
+ if ((!(channel instanceof Tp.TextChannel)) ||
+ targetHandleType != Tp.HandleType.CONTACT)
+ continue;
+
+ this._createChatSource(account, conn, channel, channel.get_target_contact());
+ }
+
+ context.accept();
+ },
+
+ _createChatSource: function(account, conn, channel, contact) {
+ if (this._chatSources[channel.get_object_path()])
+ return;
+
+ let source = new ChatSource(account, conn, channel, contact, this._tpClient);
+
+ this._chatSources[channel.get_object_path()] = source;
+ source.connect('destroy', Lang.bind(this,
+ function() {
+ if (this._tpClient.is_handling_channel(channel)) {
+ // The chat box has been destroyed so it can't
+ // handle the channel any more.
+ channel.close_async(function(src, result) {
+ channel.close_finish(result);
+ });
+ }
+
+ delete this._chatSources[channel.get_object_path()];
+ }));
+ },
+
+ _handleChannels: function(handler, account, conn, channels,
+ requests, user_action_time, context) {
+ this._handlingChannels(account, conn, channels, true);
+ context.accept();
+ },
+
+ _handlingChannels: function(account, conn, channels, notify) {
+ let len = channels.length;
+ for (let i = 0; i < len; i++) {
+ let channel = channels[i];
+
+ // We can only handle text channel, so close any other channel
+ if (!(channel instanceof Tp.TextChannel)) {
+ channel.close_async(null);
+ continue;
+ }
+
+ if (channel.get_invalidated())
+ continue;
+
+ // 'notify' will be true when coming from an actual HandleChannels
+ // call, and not when from a successful Claim call. The point is
+ // we don't want to notify for a channel we just claimed which
+ // has no new messages (for example, a new channel which only has
+ // a delivery notification). We rely on _displayPendingMessages()
+ // and _messageReceived() to notify for new messages.
+
+ // But we should still notify from HandleChannels because the
+ // Telepathy spec states that handlers must foreground channels
+ // in HandleChannels calls which are already being handled.
+
+ if (notify && this._tpClient.is_handling_channel(channel)) {
+ // We are already handling the channel, display the source
+ let source = this._chatSources[channel.get_object_path()];
+ if (source)
+ source.notify();
+ }
+ }
+ },
+
+ _displayRoomInvitation: function(conn, channel, dispatchOp, context) {
+ // We can only approve the rooms if we have been invited to it
+ let selfContact = channel.group_get_self_contact();
+ if (selfContact == null) {
+ context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
+ message: 'Not invited to the room' }));
+ return;
+ }
+
+ let [invited, inviter, reason, msg] = channel.group_get_local_pending_contact_info(selfContact);
+ if (!invited) {
+ context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
+ message: 'Not invited to the room' }));
+ return;
+ }
+
+ // FIXME: We don't have a 'chat room' icon (bgo #653737) use
+ // system-users for now as Empathy does.
+ let source = new ApproverSource(dispatchOp, _("Invitation"),
+ Gio.icon_new_for_string('system-users'));
+ Main.messageTray.add(source);
+
+ let notif = new RoomInviteNotification(source, dispatchOp, channel, inviter);
+ source.notify(notif);
+ context.accept();
+ },
+
+ _approveChannels: function(approver, account, conn, channels,
+ dispatchOp, context) {
+ let channel = channels[0];
+ let chanType = channel.get_channel_type();
+
+ if (channel.get_invalidated()) {
+ context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
+ message: 'Channel is invalidated' }));
+ return;
+ }
+
+ if (chanType == Tp.IFACE_CHANNEL_TYPE_TEXT)
+ this._approveTextChannel(account, conn, channel, dispatchOp, context);
+ else if (chanType == Tp.IFACE_CHANNEL_TYPE_CALL)
+ this._approveCall(account, conn, channel, dispatchOp, context);
+ else if (chanType == Tp.IFACE_CHANNEL_TYPE_FILE_TRANSFER)
+ this._approveFileTransfer(account, conn, channel, dispatchOp, context);
+ else
+ context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
+ message: 'Unsupported channel type' }));
+ },
+
+ _approveTextChannel: function(account, conn, channel, dispatchOp, context) {
+ let [targetHandle, targetHandleType] = channel.get_handle();
+
+ if (targetHandleType == Tp.HandleType.CONTACT) {
+ // Approve private text channels right away as we are going to handle it
+ dispatchOp.claim_with_async(this._tpClient,
+ Lang.bind(this, function(dispatchOp, result) {
+ try {
+ dispatchOp.claim_with_finish(result);
+ this._handlingChannels(account, conn, [channel], false);
+ } catch (err) {
+ throw new Error('Failed to Claim channel: ' + err);
+ }}));
+
+ context.accept();
+ } else {
+ this._displayRoomInvitation(conn, channel, dispatchOp, context);
+ }
+ },
+
+ _approveCall: function(account, conn, channel, dispatchOp, context) {
+ let isVideo = false;
+
+ let props = channel.borrow_immutable_properties();
+
+ if (props[Tp.PROP_CHANNEL_TYPE_CALL_INITIAL_VIDEO])
+ isVideo = true;
+
+ // We got the TpContact
+ let source = new ApproverSource(dispatchOp, _("Call"), isVideo ?
+ Gio.icon_new_for_string('camera-web') :
+ Gio.icon_new_for_string('audio-input-microphone'));
+ Main.messageTray.add(source);
+
+ let notif = new AudioVideoNotification(source, dispatchOp, channel,
+ channel.get_target_contact(), isVideo);
+ source.notify(notif);
+ context.accept();
+ },
+
+ _approveFileTransfer: function(account, conn, channel, dispatchOp, context) {
+ // Use the icon of the file being transferred
+ let gicon = Gio.content_type_get_icon(channel.get_mime_type());
+
+ // We got the TpContact
+ let source = new ApproverSource(dispatchOp, _("File Transfer"), gicon);
+ Main.messageTray.add(source);
+
+ let notif = new FileTransferNotification(source, dispatchOp, channel,
+ channel.get_target_contact());
+ source.notify(notif);
+ context.accept();
+ },
+
+ _delegatedChannelsCb: function(client, channels) {
+ // Nothing to do as we don't make a distinction between observed and
+ // handled channels.
+ },
+
+ _accountManagerPrepared: function(am, result) {
+ am.prepare_finish(result);
+
+ let accounts = am.get_valid_accounts();
+ for (let i = 0; i < accounts.length; i++) {
+ this._accountValidityChanged(am, accounts[i], true);
+ }
+ },
+
+ _accountValidityChanged: function(am, account, valid) {
+ if (!valid)
+ return;
+
+ // It would be better to connect to "status-changed" but we cannot.
+ // See discussion in https://bugzilla.gnome.org/show_bug.cgi?id=654159
+ account.connect("notify::connection-status",
+ Lang.bind(this, this._accountConnectionStatusNotifyCb));
+
+ account.connect('notify::connection',
+ Lang.bind(this, this._connectionChanged));
+ this._connectionChanged(account);
+ },
+
+ _connectionChanged: function(account) {
+ let conn = account.get_connection();
+ if (conn == null)
+ return;
+
+ this._tpClient.grab_contact_list_changed(conn);
+ if (conn.get_contact_list_state() == Tp.ContactListState.SUCCESS) {
+ this._contactListChanged(conn, conn.dup_contact_list(), []);
+ }
+ },
+
+ _contactListChanged: function(conn, added, removed) {
+ for (let i = 0; i < added.length; i++) {
+ let contact = added[i];
+
+ contact.connect('subscription-states-changed',
+ Lang.bind(this, this._subscriptionStateChanged));
+ this._subscriptionStateChanged(contact);
+ }
+ },
+
+ _subscriptionStateChanged: function(contact) {
+ if (contact.get_publish_state() != Tp.SubscriptionState.ASK)
+ return;
+
+ /* Implicitly accept publish requests if contact is already subscribed */
+ if (contact.get_subscribe_state() == Tp.SubscriptionState.YES ||
+ contact.get_subscribe_state() == Tp.SubscriptionState.ASK) {
+
+ contact.authorize_publication_async(function(src, result) {
+ src.authorize_publication_finish(result)});
+
+ return;
+ }
+
+ /* Display notification to ask user to accept/reject request */
+ let source = this._ensureSubscriptionSource();
+
+ let notif = new SubscriptionRequestNotification(source, contact);
+ source.notify(notif);
+ },
+
+ _ensureSubscriptionSource: function() {
+ if (this._subscriptionSource == null) {
+ this._subscriptionSource = new MessageTray.Source(_("Subscription request"),
+ 'gtk-dialog-question');
+ Main.messageTray.add(this._subscriptionSource);
+ this._subscriptionSource.connect('destroy', Lang.bind(this, function () {
+ this._subscriptionSource = null;
+ }));
+ }
+
+ return this._subscriptionSource;
+ },
+
+ _accountConnectionStatusNotifyCb: function(account) {
+ let connectionError = account.connection_error;
+
+ if (account.connection_status != Tp.ConnectionStatus.DISCONNECTED ||
+ connectionError == Tp.error_get_dbus_name(Tp.Error.CANCELLED)) {
+ return;
+ }
+
+ let notif = this._accountNotifications[account.get_object_path()];
+ if (notif)
+ return;
+
+ /* Display notification that account failed to connect */
+ let source = this._ensureAccountSource();
+
+ notif = new AccountNotification(source, account, connectionError);
+ this._accountNotifications[account.get_object_path()] = notif;
+ notif.connect('destroy', Lang.bind(this, function() {
+ delete this._accountNotifications[account.get_object_path()];
+ }));
+ source.notify(notif);
+ },
+
+ _ensureAccountSource: function() {
+ if (this._accountSource == null) {
+ this._accountSource = new MessageTray.Source(_("Connection error"),
+ 'gtk-dialog-error');
+ Main.messageTray.add(this._accountSource);
+ this._accountSource.connect('destroy', Lang.bind(this, function () {
+ this._accountSource = null;
+ }));
+ }
+
+ return this._accountSource;
+ }
+});
+
+const ChatSource = new Lang.Class({
+ Name: 'ChatSource',
+ Extends: MessageTray.Source,
+
+ _init: function(account, conn, channel, contact, client) {
+ this._account = account;
+ this._contact = contact;
+ this._client = client;
+
+ this.parent(contact.get_alias());
+
+ this.isChat = true;
+ this._pendingMessages = [];
+
+ this._conn = conn;
+ this._channel = channel;
+ this._closedId = this._channel.connect('invalidated', Lang.bind(this, this._channelClosed));
+
+ this._notification = new ChatNotification(this);
+ this._notification.setUrgency(MessageTray.Urgency.HIGH);
+ this._notifyTimeoutId = 0;
+
+ // We ack messages when the user expands the new notification or views the summary
+ // notification, in which case the notification is also expanded.
+ this._notification.connect('expanded', Lang.bind(this, this._ackMessages));
+
+ this._presence = contact.get_presence_type();
+
+ this._sentId = this._channel.connect('message-sent', Lang.bind(this, this._messageSent));
+ this._receivedId = this._channel.connect('message-received', Lang.bind(this, this._messageReceived));
+ this._pendingId = this._channel.connect('pending-message-removed', Lang.bind(this, this._pendingRemoved));
+
+ this._notifyAliasId = this._contact.connect('notify::alias', Lang.bind(this, this._updateAlias));
+ this._notifyAvatarId = this._contact.connect('notify::avatar-file', Lang.bind(this, this._updateAvatarIcon));
+ this._presenceChangedId = this._contact.connect('presence-changed', Lang.bind(this, this._presenceChanged));
+
+ // Add ourselves as a source.
+ Main.messageTray.add(this);
+ this.pushNotification(this._notification);
+
+ this._getLogMessages();
+ },
+
+ _updateAlias: function() {
+ let oldAlias = this.title;
+ let newAlias = this._contact.get_alias();
+
+ if (oldAlias == newAlias)
+ return;
+
+ this.setTitle(newAlias);
+ this._notification.appendAliasChange(oldAlias, newAlias);
+ },
+
+ createIcon: function(size) {
+ this._iconBox = new St.Bin({ style_class: 'avatar-box' });
+ this._iconBox._size = size;
+ let textureCache = St.TextureCache.get_default();
+ let file = this._contact.get_avatar_file();
+
+ if (file) {
+ let uri = file.get_uri();
+ this._iconBox.child = textureCache.load_uri_async(uri, this._iconBox._size, this._iconBox._size);
+ } else {
+ this._iconBox.child = new St.Icon({ icon_name: 'avatar-default',
+ icon_size: this._iconBox._size });
+ }
+
+ return this._iconBox;
+ },
+
+ createSecondaryIcon: function() {
+ let iconBox = new St.Bin();
+ iconBox.child = new St.Icon({ style_class: 'secondary-icon' });
+ let presenceType = this._contact.get_presence_type();
+
+ switch (presenceType) {
+ case Tp.ConnectionPresenceType.AVAILABLE:
+ iconBox.child.icon_name = 'user-available';
+ break;
+ case Tp.ConnectionPresenceType.BUSY:
+ iconBox.child.icon_name = 'user-busy';
+ break;
+ case Tp.ConnectionPresenceType.OFFLINE:
+ iconBox.child.icon_name = 'user-offline';
+ break;
+ case Tp.ConnectionPresenceType.HIDDEN:
+ iconBox.child.icon_name = 'user-invisible';
+ break;
+ case Tp.ConnectionPresenceType.AWAY:
+ iconBox.child.icon_name = 'user-away';
+ break;
+ case Tp.ConnectionPresenceType.EXTENDED_AWAY:
+ iconBox.child.icon_name = 'user-idle';
+ break;
+ default:
+ iconBox.child.icon_name = 'user-offline';
+ }
+ return iconBox;
+ },
+
+ _updateAvatarIcon: function() {
+ this.iconUpdated();
+ this._notification.update(this._notification.title, null, { customContent: true });
+ },
+
+ open: function(notification) {
+ if (this._client.is_handling_channel(this._channel)) {
+ // We are handling the channel, try to pass it to Empathy
+ this._client.delegate_channels_async([this._channel], global.get_current_time(), '', null);
+ }
+ else {
+ // We are not the handler, just ask to present the channel
+ let dbus = Tp.DBusDaemon.dup();
+ let cd = Tp.ChannelDispatcher.new(dbus);
+
+ cd.present_channel_async(this._channel, global.get_current_time(), null);
+ }
+ },
+
+ _getLogMessages: function() {
+ let logManager = Tpl.LogManager.dup_singleton();
+ let entity = Tpl.Entity.new_from_tp_contact(this._contact, Tpl.EntityType.CONTACT);
+
+ logManager.get_filtered_events_async(this._account, entity,
+ Tpl.EventTypeMask.TEXT, SCROLLBACK_HISTORY_LINES,
+ null, Lang.bind(this, this._displayPendingMessages));
+ },
+
+ _displayPendingMessages: function(logManager, result) {
+ let [success, events] = logManager.get_filtered_events_finish(result);
+
+ let logMessages = events.map(makeMessageFromTplEvent);
+
+ let pendingTpMessages = this._channel.get_pending_messages();
+ let pendingMessages = [];
+
+ for (let i = 0; i < pendingTpMessages.length; i++) {
+ let message = pendingTpMessages[i];
+
+ if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
+ continue;
+
+ pendingMessages.push(makeMessageFromTpMessage(message, NotificationDirection.RECEIVED));
+
+ this._pendingMessages.push(message);
+ }
+
+ this.countUpdated();
+
+ let showTimestamp = false;
+
+ for (let i = 0; i < logMessages.length; i++) {
+ let logMessage = logMessages[i];
+ let isPending = false;
+
+ // Skip any log messages that are also in pendingMessages
+ for (let j = 0; j < pendingMessages.length; j++) {
+ let pending = pendingMessages[j];
+ if (logMessage.timestamp == pending.timestamp && logMessage.text == pending.text) {
+ isPending = true;
+ break;
+ }
+ }
+
+ if (!isPending) {
+ showTimestamp = true;
+ this._notification.appendMessage(logMessage, true, ['chat-log-message']);
+ }
+ }
+
+ if (showTimestamp)
+ this._notification.appendTimestamp();
+
+ for (let i = 0; i < pendingMessages.length; i++)
+ this._notification.appendMessage(pendingMessages[i], true);
+
+ if (pendingMessages.length > 0)
+ this.notify();
+ },
+
+ _channelClosed: function() {
+ this._channel.disconnect(this._closedId);
+ this._channel.disconnect(this._receivedId);
+ this._channel.disconnect(this._pendingId);
+ this._channel.disconnect(this._sentId);
+
+ this._contact.disconnect(this._notifyAliasId);
+ this._contact.disconnect(this._notifyAvatarId);
+ this._contact.disconnect(this._presenceChangedId);
+
+ this.destroy();
+ },
+
+ /* All messages are new messages for Telepathy sources */
+ get count() {
+ return this._pendingMessages.length;
+ },
+
+ get unseenCount() {
+ return this.count;
+ },
+
+ get countVisible() {
+ return this.count > 0;
+ },
+
+ _messageReceived: function(channel, message) {
+ if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
+ return;
+
+ this._pendingMessages.push(message);
+ this.countUpdated();
+
+ message = makeMessageFromTpMessage(message, NotificationDirection.RECEIVED);
+ this._notification.appendMessage(message);
+
+ // Wait a bit before notifying for the received message, a handler
+ // could ack it in the meantime.
+ if (this._notifyTimeoutId != 0)
+ Mainloop.source_remove(this._notifyTimeoutId);
+ this._notifyTimeoutId = Mainloop.timeout_add(500,
+ Lang.bind(this, this._notifyTimeout));
+ },
+
+ _notifyTimeout: function() {
+ if (this._pendingMessages.length != 0)
+ this.notify();
+
+ this._notifyTimeoutId = 0;
+
+ return false;
+ },
+
+ // This is called for both messages we send from
+ // our client and other clients as well.
+ _messageSent: function(channel, message, flags, token) {
+ message = makeMessageFromTpMessage(message, NotificationDirection.SENT);
+ this._notification.appendMessage(message);
+ },
+
+ notify: function() {
+ this.parent(this._notification);
+ },
+
+ respond: function(text) {
+ let type;
+ if (text.slice(0, 4) == '/me ') {
+ type = Tp.ChannelTextMessageType.ACTION;
+ text = text.slice(4);
+ } else {
+ type = Tp.ChannelTextMessageType.NORMAL;
+ }
+
+ let msg = Tp.ClientMessage.new_text(type, text);
+ this._channel.send_message_async(msg, 0, Lang.bind(this, function (src, result) {
+ this._channel.send_message_finish(result);
+ }));
+ },
+
+ setChatState: function(state) {
+ // We don't want to send COMPOSING every time a letter is typed into
+ // the entry. We send the state only when it changes. Telepathy/Empathy
+ // might change it behind our back if the user is using both
+ // gnome-shell's entry and the Empathy conversation window. We could
+ // keep track of it with the ChatStateChanged signal but it is good
+ // enough right now.
+ if (state != this._chatState) {
+ this._chatState = state;
+ this._channel.set_chat_state_async(state, null);
+ }
+ },
+
+ _presenceChanged: function (contact, presence, status, message) {
+ let msg, title;
+
+ title = GLib.markup_escape_text(this.title, -1);
+
+ this._notification.update(this._notification.title, null, { customContent: true, secondaryIcon: this.createSecondaryIcon() });
+
+ if (message)
+ msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
+ },
+
+ _pendingRemoved: function(channel, message) {
+ let idx = this._pendingMessages.indexOf(message);
+
+ if (idx >= 0) {
+ this._pendingMessages.splice(idx, 1);
+ this.countUpdated();
+ }
+ else
+ throw new Error('Message not in our pending list: ' + message);
+ },
+
+ _ackMessages: function() {
+ // Don't clear our messages here, tp-glib will send a
+ // 'pending-message-removed' for each one.
+ this._channel.ack_all_pending_messages_async(Lang.bind(this, function(src, result) {
+ this._channel.ack_all_pending_messages_finish(result);}));
+ }
+});
+
+const ChatNotification = new Lang.Class({
+ Name: 'ChatNotification',
+ Extends: MessageTray.Notification,
+
+ _init: function(source) {
+ this.parent(source, source.title, null, { customContent: true, secondaryIcon: source.createSecondaryIcon() });
+ this.setResident(true);
+
+ this._responseEntry = new St.Entry({ style_class: 'chat-response',
+ can_focus: true });
+ this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated));
+ this._responseEntry.clutter_text.connect('text-changed', Lang.bind(this, this._onEntryChanged));
+ this.setActionArea(this._responseEntry);
+
+ this._oldMaxScrollAdjustment = 0;
+ this._createScrollArea();
+ this._lastGroup = null;
+ this._lastGroupActor = null;
+
+ this._scrollArea.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
+ let currentValue = adjustment.value + adjustment.page_size;
+ if (currentValue == this._oldMaxScrollAdjustment)
+ this.scrollTo(St.Side.BOTTOM);
+ this._oldMaxScrollAdjustment = adjustment.upper;
+ }));
+
+ this._inputHistory = new History.HistoryManager({ entry: this._responseEntry.clutter_text });
+
+ this._history = [];
+ this._timestampTimeoutId = 0;
+ this._composingTimeoutId = 0;
+ },
+
+ /**
+ * appendMessage:
+ * @message: An object with the properties:
+ * text: the body of the message,
+ * messageType: a #Tp.ChannelTextMessageType,
+ * sender: the name of the sender,
+ * timestamp: the time the message was sent
+ * direction: a #NotificationDirection
+ *
+ * @noTimestamp: Whether to add a timestamp. If %true, no timestamp
+ * will be added, regardless of the difference since the
+ * last timestamp
+ */
+ appendMessage: function(message, noTimestamp) {
+ let messageBody = GLib.markup_escape_text(message.text, -1);
+ let styles = [message.direction];
+
+ if (message.messageType == Tp.ChannelTextMessageType.ACTION) {
+ let senderAlias = GLib.markup_escape_text(message.sender, -1);
+ messageBody = '<i>%s</i> %s'.format(senderAlias, messageBody);
+ styles.push('chat-action');
+ }
+
+ if (message.direction == NotificationDirection.RECEIVED) {
+ this.update(this.source.title, messageBody, { customContent: true,
+ bannerMarkup: true });
+ }
+
+ let group = (message.direction == NotificationDirection.RECEIVED ?
+ 'received' : 'sent');
+
+ this._append({ body: messageBody,
+ group: group,
+ styles: styles,
+ timestamp: message.timestamp,
+ noTimestamp: noTimestamp });
+ },
+
+ _filterMessages: function() {
+ if (this._history.length < 1)
+ return;
+
+ let lastMessageTime = this._history[0].time;
+ let currentTime = (Date.now() / 1000);
+
+ // Keep the scrollback from growing too long. If the most
+ // recent message (before the one we just added) is within
+ // SCROLLBACK_RECENT_TIME, we will keep
+ // SCROLLBACK_RECENT_LENGTH previous messages. Otherwise
+ // we'll keep SCROLLBACK_IDLE_LENGTH messages.
+
+ let maxLength = (lastMessageTime < currentTime - SCROLLBACK_RECENT_TIME) ?
+ SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
+
+ let filteredHistory = this._history.filter(function(item) { return item.realMessage });
+ if (filteredHistory.length > maxLength) {
+ let lastMessageToKeep = filteredHistory[maxLength];
+ let expired = this._history.splice(this._history.indexOf(lastMessageToKeep));
+ for (let i = 0; i < expired.length; i++)
+ expired[i].actor.destroy();
+ }
+
+ let groups = this._contentArea.get_children();
+ for (let i = 0; i < groups.length; i++) {
+ let group = groups[i];
+ if (group.get_n_children() == 0)
+ group.destroy();
+ }
+ },
+
+ /**
+ * _append:
+ * @props: An object with the properties:
+ * body: The text of the message.
+ * group: The group of the message, one of:
+ * 'received', 'sent', 'meta'.
+ * styles: Style class names for the message to have.
+ * timestamp: The timestamp of the message.
+ * noTimestamp: suppress timestamp signal?
+ * childProps: props to add the actor with.
+ */
+ _append: function(props) {
+ let currentTime = (Date.now() / 1000);
+ props = Params.parse(props, { body: null,
+ group: null,
+ styles: [],
+ timestamp: currentTime,
+ noTimestamp: false,
+ childProps: null });
+
+ // Reset the old message timeout
+ if (this._timestampTimeoutId)
+ Mainloop.source_remove(this._timestampTimeoutId);
+
+ let highlighter = new MessageTray.URLHighlighter(props.body,
+ true, // line wrap?
+ true); // allow markup?
+
+ let body = highlighter.actor;
+
+ let styles = props.styles;
+ for (let i = 0; i < styles.length; i++)
+ body.add_style_class_name(styles[i]);
+
+ let group = props.group;
+ if (group != this._lastGroup) {
+ let style = 'chat-group-' + group;
+ this._lastGroup = group;
+ this._lastGroupActor = new St.BoxLayout({ style_class: style,
+ vertical: true });
+ this.addActor(this._lastGroupActor);
+ }
+
+ this._lastGroupActor.add(body, props.childProps);
+
+ this.updated();
+
+ let timestamp = props.timestamp;
+ this._history.unshift({ actor: body, time: timestamp,
+ realMessage: group != 'meta' });
+
+ if (!props.noTimestamp) {
+ if (timestamp < currentTime - SCROLLBACK_IMMEDIATE_TIME)
+ this.appendTimestamp();
+ else
+ // Schedule a new timestamp in SCROLLBACK_IMMEDIATE_TIME
+ // from the timestamp of the message.
+ this._timestampTimeoutId = Mainloop.timeout_add_seconds(
+ SCROLLBACK_IMMEDIATE_TIME - (currentTime - timestamp),
+ Lang.bind(this, this.appendTimestamp));
+ }
+
+ this._filterMessages();
+ },
+
+ _formatTimestamp: function(date) {
+ let now = new Date();
+
+ var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
+
+ let format;
+
+ // Show a week day and time if date is in the last week
+ if (daysAgo < 1 || (daysAgo < 7 && now.getDay() != date.getDay())) {
+ /* Translators: this is a time format string followed by a date.
+ If applicable, replace %X with a strftime format valid for your
+ locale, without seconds. */
+ // xgettext:no-c-format
+ format = _("Sent at <b>%X</b> on <b>%A</b>");
+
+ } else if (date.getYear() == now.getYear()) {
+ /* Translators: this is a time format in the style of "Wednesday, May 25",
+ shown when you get a chat message in the same year. */
+ // xgettext:no-c-format
+ format = _("Sent on <b>%A</b>, <b>%B %d</b>");
+ } else {
+ /* Translators: this is a time format in the style of "Wednesday, May 25, 2012",
+ shown when you get a chat message in a different year. */
+ // xgettext:no-c-format
+ format = _("Sent on <b>%A</b>, <b>%B %d</b>, %Y");
+ }
+
+ return date.toLocaleFormat(format);
+ },
+
+ appendTimestamp: function() {
+ let lastMessageTime = this._history[0].time;
+ let lastMessageDate = new Date(lastMessageTime * 1000);
+
+ let timeLabel = this._append({ body: this._formatTimestamp(lastMessageDate),
+ group: 'meta',
+ styles: ['chat-meta-message'],
+ childProps: { expand: true, x_fill: false,
+ x_align: St.Align.END },
+ noTimestamp: true,
+ timestamp: lastMessageTime });
+
+ this._filterMessages();
+
+ return false;
+ },
+
+ appendAliasChange: function(oldAlias, newAlias) {
+ oldAlias = GLib.markup_escape_text(oldAlias, -1);
+ newAlias = GLib.markup_escape_text(newAlias, -1);
+
+ /* Translators: this is the other person changing their old IM name to their new
+ IM name. */
+ let message = '<i>' + _("%s is now known as %s").format(oldAlias, newAlias) + '</i>';
+
+ let label = this._append({ body: message,
+ group: 'meta',
+ styles: ['chat-meta-message'] });
+
+ this.update(newAlias, null, { customContent: true });
+
+ this._filterMessages();
+ },
+
+ _onEntryActivated: function() {
+ let text = this._responseEntry.get_text();
+ if (text == '')
+ return;
+
+ this._inputHistory.addItem(text);
+
+ // Telepathy sends out the Sent signal for us.
+ // see Source._messageSent
+ this._responseEntry.set_text('');
+ this.source.respond(text);
+ },
+
+ _composingStopTimeout: function() {
+ this._composingTimeoutId = 0;
+
+ this.source.setChatState(Tp.ChannelChatState.PAUSED);
+
+ return false;
+ },
+
+ _onEntryChanged: function() {
+ let text = this._responseEntry.get_text();
+
+ // If we're typing, we want to send COMPOSING.
+ // If we empty the entry, we want to send ACTIVE.
+ // If we've stopped typing for COMPOSING_STOP_TIMEOUT
+ // seconds, we want to send PAUSED.
+
+ // Remove composing timeout.
+ if (this._composingTimeoutId > 0) {
+ Mainloop.source_remove(this._composingTimeoutId);
+ this._composingTimeoutId = 0;
+ }
+
+ if (text != '') {
+ this.source.setChatState(Tp.ChannelChatState.COMPOSING);
+
+ this._composingTimeoutId = Mainloop.timeout_add_seconds(
+ COMPOSING_STOP_TIMEOUT,
+ Lang.bind(this, this._composingStopTimeout));
+ } else {
+ this.source.setChatState(Tp.ChannelChatState.ACTIVE);
+ }
+ }
+});
+
+const ApproverSource = new Lang.Class({
+ Name: 'ApproverSource',
+ Extends: MessageTray.Source,
+
+ _init: function(dispatchOp, text, gicon) {
+ this._gicon = gicon;
+
+ this.parent(text);
+
+ this._dispatchOp = dispatchOp;
+
+ // Destroy the source if the channel dispatch operation is invalidated
+ // as we can't approve any more.
+ this._invalidId = dispatchOp.connect('invalidated',
+ Lang.bind(this, function(domain, code, msg) {
+ this.destroy();
+ }));
+ },
+
+ destroy: function() {
+ if (this._invalidId != 0) {
+ this._dispatchOp.disconnect(this._invalidId);
+ this._invalidId = 0;
+ }
+
+ this.parent();
+ },
+
+ createIcon: function(size) {
+ return new St.Icon({ gicon: this._gicon,
+ icon_size: size });
+ }
+});
+
+const RoomInviteNotification = new Lang.Class({
+ Name: 'RoomInviteNotification',
+ Extends: MessageTray.Notification,
+
+ _init: function(source, dispatchOp, channel, inviter) {
+ this.parent(source,
+ /* translators: argument is a room name like
+ * room@jabber.org for example. */
+ _("Invitation to %s").format(channel.get_identifier()),
+ null,
+ { customContent: true });
+ this.setResident(true);
+
+ /* translators: first argument is the name of a contact and the second
+ * one the name of a room. "Alice is inviting you to join room@jabber.org
+ * for example. */
+ this.addBody(_("%s is inviting you to join %s").format(inviter.get_alias(), channel.get_identifier()));
+
+ this.addButton('decline', _("Decline"));
+ this.addButton('accept', _("Accept"));
+
+ this.connect('action-invoked', Lang.bind(this, function(self, action) {
+ switch (action) {
+ case 'decline':
+ dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
+ '', function(src, result) {
+ src.leave_channels_finish(result)});
+ break;
+ case 'accept':
+ dispatchOp.handle_with_time_async('', global.get_current_time(),
+ function(src, result) {
+ src.handle_with_time_finish(result)});
+ break;
+ }
+ this.destroy();
+ }));
+ }
+});
+
+// Audio Video
+const AudioVideoNotification = new Lang.Class({
+ Name: 'AudioVideoNotification',
+ Extends: MessageTray.Notification,
+
+ _init: function(source, dispatchOp, channel, contact, isVideo) {
+ let title = '';
+
+ if (isVideo)
+ /* translators: argument is a contact name like Alice for example. */
+ title = _("Video call from %s").format(contact.get_alias());
+ else
+ /* translators: argument is a contact name like Alice for example. */
+ title = _("Call from %s").format(contact.get_alias());
+
+ this.parent(source, title, null, { customContent: true });
+ this.setResident(true);
+
+ this.addButton('reject', _("Reject"));
+ /* translators: this is a button label (verb), not a noun */
+ this.addButton('answer', _("Answer"));
+
+ this.connect('action-invoked', Lang.bind(this, function(self, action) {
+ switch (action) {
+ case 'reject':
+ dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
+ '', function(src, result) {
+ src.leave_channels_finish(result)});
+ break;
+ case 'answer':
+ dispatchOp.handle_with_time_async('', global.get_current_time(),
+ function(src, result) {
+ src.handle_with_time_finish(result)});
+ break;
+ }
+ this.destroy();
+ }));
+ }
+});
+
+// File Transfer
+const FileTransferNotification = new Lang.Class({
+ Name: 'FileTransferNotification',
+ Extends: MessageTray.Notification,
+
+ _init: function(source, dispatchOp, channel, contact) {
+ this.parent(source,
+ /* To translators: The first parameter is
+ * the contact's alias and the second one is the
+ * file name. The string will be something
+ * like: "Alice is sending you test.ogg"
+ */
+ _("%s is sending you %s").format(contact.get_alias(),
+ channel.get_filename()),
+ null,
+ { customContent: true });
+ this.setResident(true);
+
+ this.addButton('decline', _("Decline"));
+ this.addButton('accept', _("Accept"));
+
+ this.connect('action-invoked', Lang.bind(this, function(self, action) {
+ switch (action) {
+ case 'decline':
+ dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
+ '', function(src, result) {
+ src.leave_channels_finish(result)});
+ break;
+ case 'accept':
+ dispatchOp.handle_with_time_async('', global.get_current_time(),
+ function(src, result) {
+ src.handle_with_time_finish(result)});
+ break;
+ }
+ this.destroy();
+ }));
+ }
+});
+
+// Subscription request
+const SubscriptionRequestNotification = new Lang.Class({
+ Name: 'SubscriptionRequestNotification',
+ Extends: MessageTray.Notification,
+
+ _init: function(source, contact) {
+ this.parent(source,
+ /* To translators: The parameter is the contact's alias */
+ _("%s would like permission to see when you are online").format(contact.get_alias()),
+ null, { customContent: true });
+
+ this._contact = contact;
+ this._connection = contact.get_connection();
+
+ let layout = new St.BoxLayout({ vertical: false });
+
+ // Display avatar
+ let iconBox = new St.Bin({ style_class: 'avatar-box' });
+ iconBox._size = 48;
+
+ let textureCache = St.TextureCache.get_default();
+ let file = contact.get_avatar_file();
+
+ if (file) {
+ let uri = file.get_uri();
+ iconBox.child = textureCache.load_uri_async(uri, iconBox._size, iconBox._size);
+ }
+ else {
+ iconBox.child = new St.Icon({ icon_name: 'avatar-default',
+ icon_size: iconBox._size });
+ }
+
+ layout.add(iconBox);
+
+ // subscription request message
+ let label = new St.Label({ style_class: 'subscription-message',
+ text: contact.get_publish_request() });
+
+ layout.add(label);
+
+ this.addActor(layout);
+
+ this.addButton('decline', _("Decline"));
+ this.addButton('accept', _("Accept"));
+
+ this.connect('action-invoked', Lang.bind(this, function(self, action) {
+ switch (action) {
+ case 'decline':
+ contact.remove_async(function(src, result) {
+ src.remove_finish(result)});
+ break;
+ case 'accept':
+ // Authorize the contact and request to see his status as well
+ contact.authorize_publication_async(function(src, result) {
+ src.authorize_publication_finish(result)});
+
+ contact.request_subscription_async('', function(src, result) {
+ src.request_subscription_finish(result)});
+ break;
+ }
+
+ // rely on _subscriptionStatesChangedCb to destroy the
+ // notification
+ }));
+
+ this._changedId = contact.connect('subscription-states-changed',
+ Lang.bind(this, this._subscriptionStatesChangedCb));
+ this._invalidatedId = this._connection.connect('invalidated',
+ Lang.bind(this, this.destroy));
+ },
+
+ destroy: function() {
+ if (this._changedId != 0) {
+ this._contact.disconnect(this._changedId);
+ this._changedId = 0;
+ }
+
+ if (this._invalidatedId != 0) {
+ this._connection.disconnect(this._invalidatedId);
+ this._invalidatedId = 0;
+ }
+
+ this.parent();
+ },
+
+ _subscriptionStatesChangedCb: function(contact, subscribe, publish, msg) {
+ // Destroy the notification if the subscription request has been
+ // answered
+ if (publish != Tp.SubscriptionState.ASK)
+ this.destroy();
+ }
+});
+
+// Messages from empathy/libempathy/empathy-utils.c
+// create_errors_to_message_hash()
+
+/* Translator note: these should be the same messages that are
+ * used in Empathy, so just copy and paste from there. */
+let _connectionErrorMessages = {};
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.NETWORK_ERROR)]
+ = _("Network error");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.AUTHENTICATION_FAILED)]
+ = _("Authentication failed");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_ERROR)]
+ = _("Encryption error");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_PROVIDED)]
+ = _("Certificate not provided");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_UNTRUSTED)]
+ = _("Certificate untrusted");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_EXPIRED)]
+ = _("Certificate expired");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_ACTIVATED)]
+ = _("Certificate not activated");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_HOSTNAME_MISMATCH)]
+ = _("Certificate hostname mismatch");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_FINGERPRINT_MISMATCH)]
+ = _("Certificate fingerprint mismatch");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_SELF_SIGNED)]
+ = _("Certificate self-signed");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CANCELLED)]
+ = _("Status is set to offline");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_NOT_AVAILABLE)]
+ = _("Encryption is not available");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INVALID)]
+ = _("Certificate is invalid");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REFUSED)]
+ = _("Connection has been refused");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_FAILED)]
+ = _("Connection can't be established");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_LOST)]
+ = _("Connection has been lost");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ALREADY_CONNECTED)]
+ = _("This account is already connected to the server");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REPLACED)]
+ = _("Connection has been replaced by a new connection using the same resource");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.REGISTRATION_EXISTS)]
+ = _("The account already exists on the server");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.SERVICE_BUSY)]
+ = _("Server is currently too busy to handle the connection");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_REVOKED)]
+ = _("Certificate has been revoked");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INSECURE)]
+ = _("Certificate uses an insecure cipher algorithm or is cryptographically weak");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_LIMIT_EXCEEDED)]
+ = _("The length of the server certificate, or the depth of the server certificate chain, exceed the limits imposed by the cryptography library");
+_connectionErrorMessages['org.freedesktop.DBus.Error.NoReply']
+ = _("Internal error");
+
+const AccountNotification = new Lang.Class({
+ Name: 'AccountNotification',
+ Extends: MessageTray.Notification,
+
+ _init: function(source, account, connectionError) {
+ this.parent(source,
+ /* translators: argument is the account name, like
+ * name@jabber.org for example. */
+ _("Connection to %s failed").format(account.get_display_name()),
+ null, { customContent: true });
+
+ this._label = new St.Label();
+ this.addActor(this._label);
+ this._updateMessage(connectionError);
+
+ this._account = account;
+
+ this.addButton('reconnect', _("Reconnect"));
+ this.addButton('edit', _("Edit account"));
+
+ this.connect('action-invoked', Lang.bind(this, function(self, action) {
+ switch (action) {
+ case 'reconnect':
+ // If it fails again, a new notification should pop up with the
+ // new error.
+ account.reconnect_async(null);
+ break;
+ case 'edit':
+ let cmd = '/usr/bin/empathy-accounts'
+ + ' --select-account=%s'
+ .format(account.get_path_suffix());
+ let app_info = Gio.app_info_create_from_commandline(cmd, null, 0);
+ app_info.launch([], global.create_app_launch_context());
+ break;
+ }
+ this.destroy();
+ }));
+
+ this._enabledId = account.connect('notify::enabled',
+ Lang.bind(this, function() {
+ if (!account.is_enabled())
+ this.destroy();
+ }));
+
+ this._invalidatedId = account.connect('invalidated',
+ Lang.bind(this, this.destroy));
+
+ this._connectionStatusId = account.connect('notify::connection-status',
+ Lang.bind(this, function() {
+ let status = account.connection_status;
+ if (status == Tp.ConnectionStatus.CONNECTED) {
+ this.destroy();
+ } else if (status == Tp.ConnectionStatus.DISCONNECTED) {
+ this._updateMessage(account.connection_error);
+ }
+ }));
+ },
+
+ _updateMessage: function(connectionError) {
+ let message;
+ if (connectionError in _connectionErrorMessages) {
+ message = _connectionErrorMessages[connectionError];
+ } else {
+ message = _("Unknown reason");
+ }
+ this._label.set_text(message);
+ },
+
+ destroy: function() {
+ if (this._enabledId != 0) {
+ this._account.disconnect(this._enabledId);
+ this._enabledId = 0;
+ }
+
+ if (this._invalidatedId != 0) {
+ this._account.disconnect(this._invalidatedId);
+ this._invalidatedId = 0;
+ }
+
+ if (this._connectionStatusId != 0) {
+ this._account.disconnect(this._connectionStatusId);
+ this._connectionStatusId = 0;
+ }
+
+ this.parent();
+ }
+});
+const Component = TelepathyClient;
+++ /dev/null
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-
-const Lang = imports.lang;
-const Shell = imports.gi.Shell;
-const Clutter = imports.gi.Clutter;
-const St = imports.gi.St;
-const Pango = imports.gi.Pango;
-const Gio = imports.gi.Gio;
-const GObject = imports.gi.GObject;
-const Gcr = imports.gi.Gcr;
-
-const ModalDialog = imports.ui.modalDialog;
-const ShellEntry = imports.ui.shellEntry;
-const CheckBox = imports.ui.checkBox;
-
-let prompter = null;
-
-const KeyringDialog = new Lang.Class({
- Name: 'KeyringDialog',
- Extends: ModalDialog.ModalDialog,
-
- _init: function() {
- this.parent({ styleClass: 'prompt-dialog' });
-
- this.prompt = new Shell.KeyringPrompt();
- this.prompt.connect('show-password', Lang.bind(this, this._onShowPassword));
- this.prompt.connect('show-confirm', Lang.bind(this, this._onShowConfirm));
- this.prompt.connect('hide-prompt', Lang.bind(this, this._onHidePrompt));
-
- let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
- vertical: false });
- this.contentLayout.add(mainContentBox);
-
- let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
- mainContentBox.add(icon,
- { x_fill: true,
- y_fill: false,
- x_align: St.Align.END,
- y_align: St.Align.START });
-
- this._messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
- vertical: true });
- mainContentBox.add(this._messageBox,
- { y_align: St.Align.START, expand: true, x_fill: true, y_fill: true });
-
- let subject = new St.Label({ style_class: 'prompt-dialog-headline' });
- this.prompt.bind_property('message', subject, 'text', GObject.BindingFlags.SYNC_CREATE);
-
- this._messageBox.add(subject,
- { y_fill: false,
- y_align: St.Align.START });
-
- let description = new St.Label({ style_class: 'prompt-dialog-description' });
- description.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
- description.clutter_text.line_wrap = true;
- this.prompt.bind_property('description', description, 'text', GObject.BindingFlags.SYNC_CREATE);
- this._messageBox.add(description,
- { y_fill: true,
- y_align: St.Align.START });
-
- this._controlTable = null;
-
- let buttons = [{ label: '',
- action: Lang.bind(this, this._onCancelButton),
- key: Clutter.Escape
- },
- { label: '',
- action: Lang.bind(this, this._onContinueButton),
- default: true
- }]
-
- this.setButtons(buttons);
- this._cancelButton = buttons[0].button;
- this._continueButton = buttons[1].button;
-
- this.prompt.bind_property('cancel-label', this._cancelButton, 'label', GObject.BindingFlags.SYNC_CREATE);
- this.prompt.bind_property('continue-label', this._continueButton, 'label', GObject.BindingFlags.SYNC_CREATE);
- },
-
- _buildControlTable: function() {
- let table = new St.Table({ style_class: 'keyring-dialog-control-table' });
- let row = 0;
-
- if (this.prompt.password_visible) {
- let label = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
- label.set_text(_("Password:"));
- table.add(label, { row: row, col: 0,
- x_expand: false, x_fill: true,
- x_align: St.Align.START,
- y_fill: false, y_align: St.Align.MIDDLE });
- this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
- text: '',
- can_focus: true});
- this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
- ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true });
- this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onPasswordActivate));
- table.add(this._passwordEntry, { row: row, col: 1, x_expand: true, x_fill: true, x_align: St.Align.START });
- row++;
- } else {
- this._passwordEntry = null;
- }
-
- if (this.prompt.confirm_visible) {
- var label = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
- label.set_text(_("Type again:"));
- table.add(label, { row: row, col: 0,
- x_expand: false, x_fill: true,
- x_align: St.Align.START,
- y_fill: false, y_align: St.Align.MIDDLE });
- this._confirmEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
- text: '',
- can_focus: true});
- this._confirmEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
- ShellEntry.addContextMenu(this._confirmEntry, { isPassword: true });
- this._confirmEntry.clutter_text.connect('activate', Lang.bind(this, this._onConfirmActivate));
- table.add(this._confirmEntry, { row: row, col: 1, x_expand: true, x_fill: true, x_align: St.Align.START });
- row++;
- } else {
- this._confirmEntry = null;
- }
-
- this.prompt.set_password_actor(this._passwordEntry ? this._passwordEntry.clutter_text : null);
- this.prompt.set_confirm_actor(this._confirmEntry ? this._confirmEntry.clutter_text : null);
-
- if (this.prompt.choice_visible) {
- let choice = new CheckBox.CheckBox();
- this.prompt.bind_property('choice-label', choice.getLabelActor(), 'text', GObject.BindingFlags.SYNC_CREATE);
- this.prompt.bind_property('choice-chosen', choice.actor, 'checked', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);
- table.add(choice.actor, { row: row, col: 1, x_expand: false, x_fill: true, x_align: St.Align.START });
- row++;
- }
-
- let warning = new St.Label({ style_class: 'prompt-dialog-error-label' });
- warning.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
- warning.clutter_text.line_wrap = true;
- table.add(warning, { row: row, col: 1, x_expand: false, x_fill: false, x_align: St.Align.START });
- this.prompt.bind_property('warning-visible', warning, 'visible', GObject.BindingFlags.SYNC_CREATE);
- this.prompt.bind_property('warning', warning, 'text', GObject.BindingFlags.SYNC_CREATE);
-
- if (this._controlTable) {
- this._controlTable.destroy_all_children();
- this._controlTable.destroy();
- }
-
- this._controlTable = table;
- this._messageBox.add(table, { x_fill: true, y_fill: true });
- },
-
- _ensureOpen: function() {
- // NOTE: ModalDialog.open() is safe to call if the dialog is
- // already open - it just returns true without side-effects
- if (this.open())
- return true;
-
- // The above fail if e.g. unable to get input grab
- //
- // In an ideal world this wouldn't happen (because the
- // Shell is in complete control of the session) but that's
- // just not how things work right now.
-
- log('keyringPrompt: Failed to show modal dialog.' +
- ' Dismissing prompt request');
- this.prompt.cancel()
- return false;
- },
-
- _onShowPassword: function(prompt) {
- this._buildControlTable();
- this._ensureOpen();
- this._passwordEntry.grab_key_focus();
- },
-
- _onShowConfirm: function(prompt) {
- this._buildControlTable();
- this._ensureOpen();
- this._continueButton.grab_key_focus();
- },
-
- _onHidePrompt: function(prompt) {
- this.close();
- },
-
- _onPasswordActivate: function() {
- if (this.prompt.confirm_visible)
- this._confirmEntry.grab_key_focus();
- else
- this._onContinueButton();
- },
-
- _onConfirmActivate: function() {
- this._onContinueButton();
- },
-
- _onContinueButton: function() {
- this.prompt.complete()
- },
-
- _onCancelButton: function() {
- this.prompt.cancel()
- },
-});
-
-function init() {
- prompter = new Gcr.SystemPrompter();
- prompter.connect('new-prompt', function(prompter) {
- let dialog = new KeyringDialog();
- return dialog.prompt;
- });
-
- let connection = Gio.DBus.session;
- prompter.register(connection);
- Gio.bus_own_name_on_connection (connection, 'org.gnome.keyring.SystemPrompter',
- Gio.BusNameOwnerFlags.REPLACE, null, null);
-}
const Shell = imports.gi.Shell;
const St = imports.gi.St;
-const AutomountManager = imports.ui.automountManager;
-const AutorunManager = imports.ui.autorunManager;
+const Components = imports.ui.components;
const CtrlAltTab = imports.ui.ctrlAltTab;
const EndSessionDialog = imports.ui.endSessionDialog;
-const PolkitAuthenticationAgent = imports.ui.polkitAuthenticationAgent;
-const KeyringPrompt = imports.ui.keyringPrompt;
const Environment = imports.ui.environment;
const ExtensionSystem = imports.ui.extensionSystem;
const ExtensionDownloader = imports.ui.extensionDownloader;
const RunDialog = imports.ui.runDialog;
const Layout = imports.ui.layout;
const LookingGlass = imports.ui.lookingGlass;
-const NetworkAgent = imports.ui.networkAgent;
const NotificationDaemon = imports.ui.notificationDaemon;
const WindowAttentionHandler = imports.ui.windowAttentionHandler;
const ScreenShield = imports.ui.screenShield;
const SessionMode = imports.ui.sessionMode;
const ShellDBus = imports.ui.shellDBus;
const ShellMountOperation = imports.ui.shellMountOperation;
-const TelepathyClient = imports.ui.telepathyClient;
const UnlockDialog = imports.ui.unlockDialog;
const WindowManager = imports.ui.windowManager;
const Magnifier = imports.ui.magnifier;
const OVERRIDES_SCHEMA = 'org.gnome.shell.overrides';
const DEFAULT_BACKGROUND_COLOR = Clutter.Color.from_pixel(0x2266bbff);
-let automountManager = null;
-let autorunManager = null;
+let componentManager = null;
let panel = null;
let overview = null;
let runDialog = null;
let screenShield = null;
let notificationDaemon = null;
let windowAttentionHandler = null;
-let telepathyClient = null;
let ctrlAltTabManager = null;
-let recorder = null;
let sessionMode = null;
let shellDBusService = null;
let shellMountOpDBusService = null;
let xdndHandler = null;
let keyboard = null;
let layoutManager = null;
-let networkAgent = null;
let _startDate;
let _defaultCssStylesheet = null;
let _cssStylesheet = null;
let background = null;
-function createUserSession() {
- telepathyClient = new TelepathyClient.Client();
- automountManager = new AutomountManager.AutomountManager();
- autorunManager = new AutorunManager.AutorunManager();
- networkAgent = new NetworkAgent.NetworkAgent();
-
- _initRecorder();
-}
-
-function createGDMSession() {
- screenShield.showDialog();
-}
-
function createGDMLoginDialog(parentActor) {
// We do this this here instead of at the top to prevent GDM
// related code from getting loaded in normal user sessions
return [dialog, false];
}
-function createInitialSetupSession() {
- networkAgent = new NetworkAgent.NetworkAgent();
-}
-
-function _initRecorder() {
- let recorderSettings = new Gio.Settings({ schema: 'org.gnome.shell.recorder' });
- let desktopLockdownSettings = new Gio.Settings({ schema: 'org.gnome.desktop.lockdown' });
- let bindingSettings = new Gio.Settings({ schema: 'org.gnome.shell.keybindings' });
-
- global.display.add_keybinding('toggle-recording',
- bindingSettings,
- Meta.KeyBindingFlags.NONE, function() {
- if (recorder == null) {
- recorder = new Shell.Recorder({ stage: global.stage });
- }
-
- if (recorder.is_recording()) {
- recorder.close();
- Meta.enable_unredirect_for_screen(global.screen);
- } else if (!desktopLockdownSettings.get_boolean('disable-save-to-disk')) {
- // read the parameters from GSettings always in case they have changed
- recorder.set_framerate(recorderSettings.get_int('framerate'));
- /* Translators: this is a filename used for screencast recording */
- // xgettext:no-c-format
- recorder.set_filename(_("Screencast from %d %t") + '.' + recorderSettings.get_string('file-extension'));
- let pipeline = recorderSettings.get_string('pipeline');
-
- if (!pipeline.match(/^\s*$/))
- recorder.set_pipeline(pipeline);
- else
- recorder.set_pipeline(null);
-
- Meta.disable_unredirect_for_screen(global.screen);
- recorder.record();
- }
- });
-}
-
function _sessionUpdated() {
Meta.keybindings_set_custom_handler('panel-run-dialog', sessionMode.hasRunDialog ? openRunDialog : null);
- loadTheme();
}
function start() {
keyboard = new Keyboard.Keyboard();
notificationDaemon = new NotificationDaemon.NotificationDaemon();
windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
-
- sessionMode.createSession();
+ componentManager = new Components.ComponentManager();
layoutManager.init();
keyboard.init();
// initiate logouts.
EndSessionDialog.init();
- // Attempt to become a PolicyKit authentication agent
- PolkitAuthenticationAgent.init()
-
- // Become a prompter for gnome keyring
- KeyringPrompt.init();
-
_startDate = new Date();
global.stage.connect('captured-event', _globalKeyPressHandler);
+++ /dev/null
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/*
- * Copyright 2011 Giovanni Campagna <scampa.giovanni@gmail.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2, or (at your option)
- * any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
- * 02111-1307, USA.
- *
- */
-
-const Clutter = imports.gi.Clutter;
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const GObject = imports.gi.GObject;
-const Lang = imports.lang;
-const NetworkManager = imports.gi.NetworkManager;
-const NMClient = imports.gi.NMClient;
-const Pango = imports.gi.Pango;
-const Shell = imports.gi.Shell;
-const St = imports.gi.St;
-
-const Config = imports.misc.config;
-const ModalDialog = imports.ui.modalDialog;
-const PopupMenu = imports.ui.popupMenu;
-const ShellEntry = imports.ui.shellEntry;
-
-const VPN_UI_GROUP = 'VPN Plugin UI';
-
-const NetworkSecretDialog = new Lang.Class({
- Name: 'NetworkSecretDialog',
- Extends: ModalDialog.ModalDialog,
-
- _init: function(agent, requestId, connection, settingName, hints, contentOverride) {
- this.parent({ styleClass: 'prompt-dialog' });
-
- this._agent = agent;
- this._requestId = requestId;
- this._connection = connection;
- this._settingName = settingName;
- this._hints = hints;
-
- if (contentOverride)
- this._content = contentOverride;
- else
- this._content = this._getContent();
-
- let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
- vertical: false });
- this.contentLayout.add(mainContentBox,
- { x_fill: true,
- y_fill: true });
-
- let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
- mainContentBox.add(icon,
- { x_fill: true,
- y_fill: false,
- x_align: St.Align.END,
- y_align: St.Align.START });
-
- let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
- vertical: true });
- mainContentBox.add(messageBox,
- { y_align: St.Align.START });
-
- let subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline',
- text: this._content.title });
- messageBox.add(subjectLabel,
- { y_fill: false,
- y_align: St.Align.START });
-
- if (this._content.message != null) {
- let descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description',
- text: this._content.message,
- // HACK: for reasons unknown to me, the label
- // is not asked the correct height for width,
- // and thus is underallocated
- // place a fixed height to avoid overflowing
- style: 'height: 3em'
- });
- descriptionLabel.clutter_text.line_wrap = true;
-
- messageBox.add(descriptionLabel,
- { y_fill: true,
- y_align: St.Align.START,
- expand: true });
- }
-
- let secretTable = new St.Table({ style_class: 'network-dialog-secret-table' });
- let initialFocusSet = false;
- let pos = 0;
- for (let i = 0; i < this._content.secrets.length; i++) {
- let secret = this._content.secrets[i];
- let label = new St.Label({ style_class: 'prompt-dialog-password-label',
- text: secret.label });
-
- let reactive = secret.key != null;
-
- secret.entry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
- text: secret.value, can_focus: reactive,
- reactive: reactive });
- ShellEntry.addContextMenu(secret.entry,
- { isPassword: secret.password });
-
- if (secret.validate)
- secret.valid = secret.validate(secret);
- else // no special validation, just ensure it's not empty
- secret.valid = secret.value.length > 0;
-
- if (reactive) {
- if (!initialFocusSet) {
- this.setInitialKeyFocus(secret.entry);
- initialFocusSet = true;
- }
-
- secret.entry.clutter_text.connect('activate', Lang.bind(this, this._onOk));
- secret.entry.clutter_text.connect('text-changed', Lang.bind(this, function() {
- secret.value = secret.entry.get_text();
- if (secret.validate)
- secret.valid = secret.validate(secret);
- else
- secret.valid = secret.value.length > 0;
- this._updateOkButton();
- }));
- } else
- secret.valid = true;
-
- secretTable.add(label, { row: pos, col: 0,
- x_expand: false, x_fill: true,
- x_align: St.Align.START,
- y_fill: false, y_align: St.Align.MIDDLE });
- secretTable.add(secret.entry, { row: pos, col: 1, x_expand: true, x_fill: true, y_align: St.Align.END });
- pos++;
-
- if (secret.password)
- secret.entry.clutter_text.set_password_char('\u25cf');
- }
-
- messageBox.add(secretTable);
-
- this._okButton = { label: _("Connect"),
- action: Lang.bind(this, this._onOk),
- default: true
- };
-
- this.setButtons([{ label: _("Cancel"),
- action: Lang.bind(this, this.cancel),
- key: Clutter.KEY_Escape,
- },
- this._okButton]);
- },
-
- _updateOkButton: function() {
- let valid = true;
- for (let i = 0; i < this._content.secrets.length; i++) {
- let secret = this._content.secrets[i];
- valid = valid && secret.valid;
- }
-
- this._okButton.button.reactive = valid;
- this._okButton.button.can_focus = valid;
- },
-
- _onOk: function() {
- let valid = true;
- for (let i = 0; i < this._content.secrets.length; i++) {
- let secret = this._content.secrets[i];
- valid = valid && secret.valid;
- if (secret.key != null)
- this._agent.set_password(this._requestId, secret.key, secret.value);
- }
-
- if (valid) {
- this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
- this.close(global.get_current_time());
- }
- // do nothing if not valid
- },
-
- cancel: function() {
- this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
- this.close(global.get_current_time());
- },
-
- _validateWpaPsk: function(secret) {
- let value = secret.value;
- if (value.length == 64) {
- // must be composed of hexadecimal digits only
- for (let i = 0; i < 64; i++) {
- if (!((value[i] >= 'a' && value[i] <= 'f')
- || (value[i] >= 'A' && value[i] <= 'F')
- || (value[i] >= '0' && value[i] <= '9')))
- return false;
- }
- return true;
- }
-
- return (value.length >= 8 && value.length <= 63);
- },
-
- _validateStaticWep: function(secret) {
- let value = secret.value;
- if (secret.wep_key_type == NetworkManager.WepKeyType.KEY) {
- if (value.length == 10 || value.length == 26) {
- for (let i = 0; i < value.length; i++) {
- if (!((value[i] >= 'a' && value[i] <= 'f')
- || (value[i] >= 'A' && value[i] <= 'F')
- || (value[i] >= '0' && value[i] <= '9')))
- return false;
- }
- } else if (value.length == 5 || value.length == 13) {
- for (let i = 0; i < value.length; i++) {
- if (!((value[i] >= 'a' && value[i] <= 'z')
- || (value[i] >= 'A' && value[i] <= 'Z')))
- return false;
- }
- } else
- return false;
- } else if (secret.wep_key_type == NetworkManager.WepKeyType.PASSPHRASE) {
- if (value.length < 0 || value.length > 64)
- return false;
- }
- return true;
- },
-
- _getWirelessSecrets: function(secrets, wirelessSetting) {
- let wirelessSecuritySetting = this._connection.get_setting_wireless_security();
- switch (wirelessSecuritySetting.key_mgmt) {
- // First the easy ones
- case 'wpa-none':
- case 'wpa-psk':
- secrets.push({ label: _("Password: "), key: 'psk',
- value: wirelessSecuritySetting.psk || '',
- validate: this._validateWpaPsk, password: true });
- break;
- case 'none': // static WEP
- secrets.push({ label: _("Key: "), key: 'wep-key' + wirelessSecuritySetting.wep_tx_keyidx,
- value: wirelessSecuritySetting.get_wep_key(wirelessSecuritySetting.wep_tx_keyidx) || '',
- wep_key_type: wirelessSecuritySetting.wep_key_type,
- validate: this._validateStaticWep, password: true });
- break;
- case 'ieee8021x':
- if (wirelessSecuritySetting.auth_alg == 'leap') // Cisco LEAP
- secrets.push({ label: _("Password: "), key: 'leap-password',
- value: wirelessSecuritySetting.leap_password || '', password: true });
- else // Dynamic (IEEE 802.1x) WEP
- this._get8021xSecrets(secrets);
- break;
- case 'wpa-eap':
- this._get8021xSecrets(secrets);
- break;
- default:
- log('Invalid wireless key management: ' + wirelessSecuritySetting.key_mgmt);
- }
- },
-
- _get8021xSecrets: function(secrets) {
- let ieee8021xSetting = this._connection.get_setting_802_1x();
- let phase2method;
-
- switch (ieee8021xSetting.get_eap_method(0)) {
- case 'md5':
- case 'leap':
- case 'ttls':
- case 'peap':
- // TTLS and PEAP are actually much more complicated, but this complication
- // is not visible here since we only care about phase2 authentication
- // (and don't even care of which one)
- secrets.push({ label: _("Username: "), key: null,
- value: ieee8021xSetting.identity || '', password: false });
- secrets.push({ label: _("Password: "), key: 'password',
- value: ieee8021xSetting.password || '', password: true });
- break;
- case 'tls':
- secrets.push({ label: _("Identity: "), key: null,
- value: ieee8021xSetting.identity || '', password: false });
- secrets.push({ label: _("Private key password: "), key: 'private-key-password',
- value: ieee8021xSetting.private_key_password || '', password: true });
- break;
- default:
- log('Invalid EAP/IEEE802.1x method: ' + ieee8021xSetting.get_eap_method(0));
- }
- },
-
- _getPPPoESecrets: function(secrets) {
- let pppoeSetting = this._connection.get_setting_pppoe();
- secrets.push({ label: _("Username: "), key: 'username',
- value: pppoeSetting.username || '', password: false });
- secrets.push({ label: _("Service: "), key: 'service',
- value: pppoeSetting.service || '', password: false });
- secrets.push({ label: _("Password: "), key: 'password',
- value: pppoeSetting.password || '', password: true });
- },
-
- _getMobileSecrets: function(secrets, connectionType) {
- let setting;
- if (connectionType == 'bluetooth')
- setting = this._connection.get_setting_cdma() || this._connection.get_setting_gsm();
- else
- setting = this._connection.get_setting_by_name(connectionType);
- secrets.push({ label: _("Password: "), key: 'password',
- value: setting.value || '', password: true });
- },
-
- _getContent: function() {
- let connectionSetting = this._connection.get_setting_connection();
- let connectionType = connectionSetting.get_connection_type();
- let wirelessSetting;
- let ssid;
-
- let content = { };
- content.secrets = [ ];
-
- switch (connectionType) {
- case '802-11-wireless':
- wirelessSetting = this._connection.get_setting_wireless();
- ssid = NetworkManager.utils_ssid_to_utf8(wirelessSetting.get_ssid());
- content.title = _("Authentication required by wireless network");
- content.message = _("Passwords or encryption keys are required to access the wireless network '%s'.").format(ssid);
- this._getWirelessSecrets(content.secrets, wirelessSetting);
- break;
- case '802-3-ethernet':
- content.title = _("Wired 802.1X authentication");
- content.message = null;
- content.secrets.push({ label: _("Network name: "), key: null,
- value: connectionSetting.get_id(), password: false });
- this._get8021xSecrets(content.secrets);
- break;
- case 'pppoe':
- content.title = _("DSL authentication");
- content.message = null;
- this._getPPPoESecrets(content.secrets);
- break;
- case 'gsm':
- if (this._hints.indexOf('pin') != -1) {
- let gsmSetting = this._connection.get_setting_gsm();
- content.title = _("PIN code required");
- content.message = _("PIN code is needed for the mobile broadband device");
- content.secrets.push({ label: _("PIN: "), key: 'pin',
- value: gsmSetting.pin || '', password: true });
- }
- // fall through
- case 'cdma':
- case 'bluetooth':
- content.title = _("Mobile broadband network password");
- content.message = _("A password is required to connect to '%s'.").format(connectionSetting.get_id());
- this._getMobileSecrets(content.secrets, connectionType);
- break;
- default:
- log('Invalid connection type: ' + connectionType);
- };
-
- return content;
- }
-});
-
-const VPNRequestHandler = new Lang.Class({
- Name: 'VPNRequestHandler',
-
- _init: function(agent, requestId, authHelper, serviceType, connection, hints, flags) {
- this._agent = agent;
- this._requestId = requestId;
- this._connection = connection;
- this._pluginOutBuffer = [];
- this._title = null;
- this._description = null;
- this._content = [ ];
- this._shellDialog = null;
-
- let connectionSetting = connection.get_setting_connection();
-
- let argv = [ authHelper.fileName,
- '-u', connectionSetting.uuid,
- '-n', connectionSetting.id,
- '-s', serviceType
- ];
- if (authHelper.externalUIMode)
- argv.push('--external-ui-mode');
- if (flags & NMClient.SecretAgentGetSecretsFlags.ALLOW_INTERACTION)
- argv.push('-i');
- if (flags & NMClient.SecretAgentGetSecretsFlags.REQUEST_NEW)
- argv.push('-r');
-
- this._newStylePlugin = authHelper.externalUIMode;
-
- try {
- let [success, pid, stdin, stdout, stderr] =
- GLib.spawn_async_with_pipes(null, /* pwd */
- argv,
- null, /* envp */
- GLib.SpawnFlags.DO_NOT_REAP_CHILD,
- null /* child_setup */);
-
- this._childPid = pid;
- this._stdin = new Gio.UnixOutputStream({ fd: stdin, close_fd: true });
- this._stdout = new Gio.UnixInputStream({ fd: stdout, close_fd: true });
- // We need this one too, even if don't actually care of what the process
- // has to say on stderr, because otherwise the fd opened by g_spawn_async_with_pipes
- // is kept open indefinitely
- let stderrStream = new Gio.UnixInputStream({ fd: stderr, close_fd: true });
- stderrStream.close(null);
- this._dataStdout = new Gio.DataInputStream({ base_stream: this._stdout });
-
- if (this._newStylePlugin)
- this._readStdoutNewStyle();
- else
- this._readStdoutOldStyle();
-
- this._childWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid,
- Lang.bind(this, this._vpnChildFinished));
-
- this._writeConnection();
- } catch(e) {
- logError(e, 'error while spawning VPN auth helper');
-
- this._agent.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
- }
- },
-
- cancel: function() {
- if (this._newStylePlugin && this._shellDialog) {
- this._shellDialog.close(global.get_current_time());
- this._shellDialog.destroy();
- } else {
- try {
- this._stdin.write('QUIT\n\n', null);
- } catch(e) { /* ignore broken pipe errors */ }
- }
-
- this.destroy();
- },
-
- destroy: function() {
- if (this._destroyed)
- return;
-
- GLib.source_remove(this._childWatch);
-
- this._stdin.close(null);
- // Stdout is closed when we finish reading from it
-
- this._destroyed = true;
- },
-
- _vpnChildFinished: function(pid, status, requestObj) {
- if (this._newStylePlugin) {
- // For new style plugin, all work is done in the async reading functions
- // Just reap the process here
- return;
- }
-
- let [exited, exitStatus] = Shell.util_wifexited(status);
-
- if (exited) {
- if (exitStatus != 0)
- this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
- else
- this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
- } else
- this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
-
- this.destroy();
- },
-
- _vpnChildProcessLineOldStyle: function(line) {
- if (this._previousLine != undefined) {
- // Two consecutive newlines mean that the child should be closed
- // (the actual newlines are eaten by Gio.DataInputStream)
- // Send a termination message
- if (line == '' && this._previousLine == '') {
- try {
- this._stdin.write('QUIT\n\n', null);
- } catch(e) { /* ignore broken pipe errors */ }
- } else {
- this._agent.set_password(this._requestId, this._previousLine, line);
- this._previousLine = undefined;
- }
- } else {
- this._previousLine = line;
- }
- },
-
- _readStdoutOldStyle: function() {
- this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
- let [line, len] = this._dataStdout.read_line_finish_utf8(result);
-
- if (line == null) {
- // end of file
- this._stdout.close(null);
- return;
- }
-
- this._vpnChildProcessLineOldStyle(line);
-
- // try to read more!
- this._readStdoutOldStyle();
- }));
- },
-
- _readStdoutNewStyle: function() {
- this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
- let cnt = this._dataStdout.fill_finish(result);
-
- if (cnt == 0) {
- // end of file
- this._showNewStyleDialog();
-
- this._stdout.close(null);
- return;
- }
-
- // Try to read more
- this._dataStdout.set_buffer_size(2 * this._dataStdout.get_buffer_size());
- this._readStdoutNewStyle();
- }));
- },
-
- _showNewStyleDialog: function() {
- let keyfile = new GLib.KeyFile();
- let contentOverride;
-
- try {
- let data = this._dataStdout.peek_buffer();
- keyfile.load_from_data(data.toString(), data.length,
- GLib.KeyFileFlags.NONE);
-
- if (keyfile.get_integer(VPN_UI_GROUP, 'Version') != 2)
- throw new Error('Invalid plugin keyfile version, is %d');
-
- contentOverride = { title: keyfile.get_string(VPN_UI_GROUP, 'Title'),
- message: keyfile.get_string(VPN_UI_GROUP, 'Description'),
- secrets: [] };
-
- let [groups, len] = keyfile.get_groups();
- for (let i = 0; i < groups.length; i++) {
- if (groups[i] == VPN_UI_GROUP)
- continue;
-
- let value = keyfile.get_string(groups[i], 'Value');
- let shouldAsk = keyfile.get_boolean(groups[i], 'ShouldAsk');
-
- if (shouldAsk) {
- contentOverride.secrets.push({ label: keyfile.get_string(groups[i], 'Label'),
- key: groups[i],
- value: value,
- password: keyfile.get_boolean(groups[i], 'IsSecret')
- });
- } else {
- if (!value.length) // Ignore empty secrets
- continue;
-
- this._agent.set_password(this._requestId, groups[i], value);
- }
- }
- } catch(e) {
- logError(e, 'error while reading VPN plugin output keyfile');
-
- this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
- return;
- }
-
- if (contentOverride.secrets.length) {
- // Only show the dialog if we actually have something to ask
- this._shellDialog = new NetworkSecretDialog(this._agent, this._requestId, this._connection, 'vpn', [], contentOverride);
- this._shellDialog.open(global.get_current_time());
- } else {
- this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
- }
- },
-
- _writeConnection: function() {
- let vpnSetting = this._connection.get_setting_vpn();
-
- try {
- vpnSetting.foreach_data_item(Lang.bind(this, function(key, value) {
- this._stdin.write('DATA_KEY=' + key + '\n', null);
- this._stdin.write('DATA_VAL=' + (value || '') + '\n\n', null);
- }));
- vpnSetting.foreach_secret(Lang.bind(this, function(key, value) {
- this._stdin.write('SECRET_KEY=' + key + '\n', null);
- this._stdin.write('SECRET_VAL=' + (value || '') + '\n\n', null);
- }));
- this._stdin.write('DONE\n\n', null);
- } catch(e) {
- logError(e, 'internal error while writing connection to helper');
-
- this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
- }
- },
-});
-
-const NetworkAgent = new Lang.Class({
- Name: 'NetworkAgent',
-
- _init: function() {
- this._native = new Shell.NetworkAgent({ auto_register: true,
- identifier: 'org.gnome.Shell.NetworkAgent' });
-
- this._dialogs = { };
- this._vpnRequests = { };
-
- this._native.connect('new-request', Lang.bind(this, this._newRequest));
- this._native.connect('cancel-request', Lang.bind(this, this._cancelRequest));
- },
-
- _newRequest: function(agent, requestId, connection, settingName, hints, flags) {
- if (settingName == 'vpn') {
- this._vpnRequest(requestId, connection, hints, flags);
- return;
- }
-
- let dialog = new NetworkSecretDialog(agent, requestId, connection, settingName, hints);
- dialog.connect('destroy', Lang.bind(this, function() {
- delete this._dialogs[requestId];
- }));
- this._dialogs[requestId] = dialog;
- dialog.open(global.get_current_time());
- },
-
- _cancelRequest: function(agent, requestId) {
- if (this._dialogs[requestId]) {
- this._dialogs[requestId].close(global.get_current_time());
- this._dialogs[requestId].destroy();
- delete this._dialogs[requestId];
- } else if (this._vpnRequests[requestId]) {
- this._vpnRequests[requestId].cancel();
- delete this._vpnRequests[requestId];
- }
- },
-
- _vpnRequest: function(requestId, connection, hints, flags) {
- let vpnSetting = connection.get_setting_vpn();
- let serviceType = vpnSetting.service_type;
-
- this._buildVPNServiceCache();
-
- let binary = this._vpnBinaries[serviceType];
- if (!binary) {
- log('Invalid VPN service type (cannot find authentication binary)');
-
- /* cancel the auth process */
- this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
- return;
- }
-
- this._vpnRequests[requestId] = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags);
- },
-
- _buildVPNServiceCache: function() {
- if (this._vpnCacheBuilt)
- return;
-
- this._vpnCacheBuilt = true;
- this._vpnBinaries = { };
-
- let dir = Gio.file_new_for_path(GLib.build_filenamev([Config.SYSCONFDIR, 'NetworkManager/VPN']));
- try {
- let fileEnum = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
- let info;
-
- while ((info = fileEnum.next_file(null))) {
- let name = info.get_name();
- if (name.substr(-5) != '.name')
- continue;
-
- try {
- let keyfile = new GLib.KeyFile();
- keyfile.load_from_file(dir.get_child(name).get_path(), GLib.KeyFileFlags.NONE);
- let service = keyfile.get_string('VPN Connection', 'service');
- let binary = keyfile.get_string('GNOME', 'auth-dialog');
- let externalUIMode = false;
- try {
- externalUIMode = keyfile.get_boolean('GNOME', 'supports-external-ui-mode');
- } catch(e) { } // ignore errors if key does not exist
- let path = binary;
- if (!GLib.path_is_absolute(path)) {
- path = GLib.build_filenamev([Config.LIBEXECDIR, path]);
- }
-
- if (GLib.file_test(path, GLib.FileTest.IS_EXECUTABLE))
- this._vpnBinaries[service] = { fileName: path, externalUIMode: externalUIMode };
- else
- throw new Error('VPN plugin at %s is not executable'.format(path));
- } catch(e) {
- log('Error \'%s\' while processing VPN keyfile \'%s\''.
- format(e.message, dir.get_child(name).get_path()));
- continue;
- }
- }
- } catch(e) {
- logError(e, 'error while enumerating VPN auth helpers');
- }
- }
-});
+++ /dev/null
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/*
- * Copyright 2010 Red Hat, Inc
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2, or (at your option)
- * any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
- * 02111-1307, USA.
-*
- * Author: David Zeuthen <davidz@redhat.com>
- */
-
-const Lang = imports.lang;
-const Signals = imports.signals;
-const Shell = imports.gi.Shell;
-const AccountsService = imports.gi.AccountsService;
-const Clutter = imports.gi.Clutter;
-const St = imports.gi.St;
-const Pango = imports.gi.Pango;
-const GLib = imports.gi.GLib;
-const Gio = imports.gi.Gio;
-const Mainloop = imports.mainloop;
-const Polkit = imports.gi.Polkit;
-const PolkitAgent = imports.gi.PolkitAgent;
-
-const ModalDialog = imports.ui.modalDialog;
-const ShellEntry = imports.ui.shellEntry;
-const UserMenu = imports.ui.userMenu;
-
-const DIALOG_ICON_SIZE = 48;
-
-const AuthenticationDialog = new Lang.Class({
- Name: 'AuthenticationDialog',
- Extends: ModalDialog.ModalDialog,
-
- _init: function(actionId, message, cookie, userNames) {
- this.parent({ styleClass: 'prompt-dialog' });
-
- this.actionId = actionId;
- this.message = message;
- this.userNames = userNames;
- this._wasDismissed = false;
- this._completed = false;
-
- let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
- vertical: false });
- this.contentLayout.add(mainContentBox,
- { x_fill: true,
- y_fill: true });
-
- let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
- mainContentBox.add(icon,
- { x_fill: true,
- y_fill: false,
- x_align: St.Align.END,
- y_align: St.Align.START });
-
- let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
- vertical: true });
- mainContentBox.add(messageBox,
- { y_align: St.Align.START });
-
- this._subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline',
- text: _("Authentication Required") });
-
- messageBox.add(this._subjectLabel,
- { y_fill: false,
- y_align: St.Align.START });
-
- this._descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description',
- text: message });
- this._descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
- this._descriptionLabel.clutter_text.line_wrap = true;
-
- messageBox.add(this._descriptionLabel,
- { y_fill: true,
- y_align: St.Align.START });
-
- if (userNames.length > 1) {
- log('polkitAuthenticationAgent: Received ' + userNames.length +
- ' identities that can be used for authentication. Only ' +
- 'considering one.');
- }
-
- let userName = GLib.get_user_name();
- if (userNames.indexOf(userName) < 0)
- userName = 'root';
- if (userNames.indexOf(userName) < 0)
- userName = userNames[0];
-
- this._user = AccountsService.UserManager.get_default().get_user(userName);
- let userRealName = this._user.get_real_name()
- this._userLoadedId = this._user.connect('notify::is_loaded',
- Lang.bind(this, this._onUserChanged));
- this._userChangedId = this._user.connect('changed',
- Lang.bind(this, this._onUserChanged));
-
- // Special case 'root'
- let userIsRoot = false;
- if (userName == 'root') {
- userIsRoot = true;
- userRealName = _("Administrator");
- }
-
- if (userIsRoot) {
- let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-root-label',
- text: userRealName }));
- messageBox.add(userLabel);
- } else {
- let userBox = new St.BoxLayout({ style_class: 'polkit-dialog-user-layout',
- vertical: false });
- messageBox.add(userBox);
- this._userAvatar = new UserMenu.UserAvatarWidget(this._user,
- { iconSize: DIALOG_ICON_SIZE,
- styleClass: 'polkit-dialog-user-icon' });
- this._userAvatar.actor.hide();
- userBox.add(this._userAvatar.actor,
- { x_fill: true,
- y_fill: false,
- x_align: St.Align.END,
- y_align: St.Align.START });
- let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-label',
- text: userRealName }));
- userBox.add(userLabel,
- { x_fill: true,
- y_fill: false,
- x_align: St.Align.END,
- y_align: St.Align.MIDDLE });
- }
-
- this._onUserChanged();
-
- this._passwordBox = new St.BoxLayout({ vertical: false, style_class: 'prompt-dialog-password-box' });
- messageBox.add(this._passwordBox);
- this._passwordLabel = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
- this._passwordBox.add(this._passwordLabel, { y_fill: false, y_align: St.Align.MIDDLE });
- this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
- text: "",
- can_focus: true});
- ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true });
- this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivate));
- this._passwordBox.add(this._passwordEntry,
- { expand: true });
- this.setInitialKeyFocus(this._passwordEntry);
- this._passwordBox.hide();
-
- this._errorMessageLabel = new St.Label({ style_class: 'prompt-dialog-error-label' });
- this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
- this._errorMessageLabel.clutter_text.line_wrap = true;
- messageBox.add(this._errorMessageLabel);
- this._errorMessageLabel.hide();
-
- this._infoMessageLabel = new St.Label({ style_class: 'prompt-dialog-info-label' });
- this._infoMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
- this._infoMessageLabel.clutter_text.line_wrap = true;
- messageBox.add(this._infoMessageLabel);
- this._infoMessageLabel.hide();
-
- /* text is intentionally non-blank otherwise the height is not the same as for
- * infoMessage and errorMessageLabel - but it is still invisible because
- * gnome-shell.css sets the color to be transparent
- */
- this._nullMessageLabel = new St.Label({ style_class: 'prompt-dialog-null-label',
- text: 'abc'});
- this._nullMessageLabel.add_style_class_name('hidden');
- this._nullMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
- this._nullMessageLabel.clutter_text.line_wrap = true;
- messageBox.add(this._nullMessageLabel);
- this._nullMessageLabel.show();
-
- this.setButtons([{ label: _("Cancel"),
- action: Lang.bind(this, this.cancel),
- key: Clutter.Escape
- },
- { label: _("Authenticate"),
- action: Lang.bind(this, this._onAuthenticateButtonPressed),
- default: true
- }]);
-
- this._doneEmitted = false;
-
- this._identityToAuth = Polkit.UnixUser.new_for_name(userName);
- this._cookie = cookie;
-
- this._session = new PolkitAgent.Session({ identity: this._identityToAuth,
- cookie: this._cookie });
- this._session.connect('completed', Lang.bind(this, this._onSessionCompleted));
- this._session.connect('request', Lang.bind(this, this._onSessionRequest));
- this._session.connect('show-error', Lang.bind(this, this._onSessionShowError));
- this._session.connect('show-info', Lang.bind(this, this._onSessionShowInfo));
- },
-
- startAuthentication: function() {
- this._session.initiate();
- },
-
- _ensureOpen: function() {
- // NOTE: ModalDialog.open() is safe to call if the dialog is
- // already open - it just returns true without side-effects
- if (!this.open(global.get_current_time())) {
- // This can fail if e.g. unable to get input grab
- //
- // In an ideal world this wouldn't happen (because the
- // Shell is in complete control of the session) but that's
- // just not how things work right now.
- //
- // One way to make this happen is by running 'sleep 3;
- // pkexec bash' and then opening a popup menu.
- //
- // We could add retrying if this turns out to be a problem
-
- log('polkitAuthenticationAgent: Failed to show modal dialog.' +
- ' Dismissing authentication request for action-id ' + this.actionId +
- ' cookie ' + this._cookie);
- this._emitDone(false, true);
- }
- },
-
- _emitDone: function(keepVisible, dismissed) {
- if (!this._doneEmitted) {
- this._doneEmitted = true;
- this.emit('done', keepVisible, dismissed);
- }
- },
-
- _onEntryActivate: function() {
- let response = this._passwordEntry.get_text();
- this._session.response(response);
- // When the user responds, dismiss already shown info and
- // error texts (if any)
- this._errorMessageLabel.hide();
- this._infoMessageLabel.hide();
- this._nullMessageLabel.show();
- },
-
- _onAuthenticateButtonPressed: function() {
- this._onEntryActivate();
- },
-
- _onSessionCompleted: function(session, gainedAuthorization) {
- if (this._completed)
- return;
-
- this._completed = true;
-
- if (!gainedAuthorization) {
- /* Unless we are showing an existing error message from the PAM
- * module (the PAM module could be reporting the authentication
- * error providing authentication-method specific information),
- * show "Sorry, that didn't work. Please try again."
- */
- if (!this._errorMessageLabel.visible && !this._wasDismissed) {
- /* Translators: "that didn't work" refers to the fact that the
- * requested authentication was not gained; this can happen
- * because of an authentication error (like invalid password),
- * for instance. */
- this._errorMessageLabel.set_text(_("Sorry, that didn\'t work. Please try again."));
- this._errorMessageLabel.show();
- this._infoMessageLabel.hide();
- this._nullMessageLabel.hide();
- }
- }
- this._emitDone(!gainedAuthorization, false);
- },
-
- _onSessionRequest: function(session, request, echo_on) {
- // Cheap localization trick
- if (request == 'Password:' || request == 'Password: ')
- this._passwordLabel.set_text(_("Password:"));
- else
- this._passwordLabel.set_text(request);
-
- if (echo_on)
- this._passwordEntry.clutter_text.set_password_char('');
- else
- this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
-
- this._passwordBox.show();
- this._passwordEntry.set_text('');
- this._passwordEntry.grab_key_focus();
- this._ensureOpen();
- },
-
- _onSessionShowError: function(session, text) {
- this._passwordEntry.set_text('');
- this._errorMessageLabel.set_text(text);
- this._errorMessageLabel.show();
- this._infoMessageLabel.hide();
- this._nullMessageLabel.hide();
- this._ensureOpen();
- },
-
- _onSessionShowInfo: function(session, text) {
- this._passwordEntry.set_text('');
- this._infoMessageLabel.set_text(text);
- this._infoMessageLabel.show();
- this._errorMessageLabel.hide();
- this._nullMessageLabel.hide();
- this._ensureOpen();
- },
-
- destroySession: function() {
- if (this._session) {
- if (!this._completed)
- this._session.cancel();
- this._session = null;
- }
- },
-
- _onUserChanged: function() {
- if (this._user.is_loaded) {
- this._userAvatar.update();
- this._userAvatar.actor.show();
- }
- },
-
- cancel: function() {
- this._wasDismissed = true;
- this.close(global.get_current_time());
- this._emitDone(false, true);
- },
-});
-Signals.addSignalMethods(AuthenticationDialog.prototype);
-
-const AuthenticationAgent = new Lang.Class({
- Name: 'AuthenticationAgent',
-
- _init: function() {
- this._native = new Shell.PolkitAuthenticationAgent();
- this._native.connect('initiate', Lang.bind(this, this._onInitiate));
- this._native.connect('cancel', Lang.bind(this, this._onCancel));
- this._currentDialog = null;
- this._isCompleting = false;
- },
-
- _onInitiate: function(nativeAgent, actionId, message, iconName, cookie, userNames) {
- this._currentDialog = new AuthenticationDialog(actionId, message, cookie, userNames);
-
- // We actually don't want to open the dialog until we know for
- // sure that we're going to interact with the user. For
- // example, if the password for the identity to auth is blank
- // (which it will be on a live CD) then there will be no
- // conversation at all... of course, we don't *know* that
- // until we actually try it.
- //
- // See https://bugzilla.gnome.org/show_bug.cgi?id=643062 for more
- // discussion.
-
- this._currentDialog.connect('done', Lang.bind(this, this._onDialogDone));
- this._currentDialog.startAuthentication();
- },
-
- _onCancel: function(nativeAgent) {
- this._completeRequest(false, false);
- },
-
- _onDialogDone: function(dialog, keepVisible, dismissed) {
- this._completeRequest(keepVisible, dismissed);
- },
-
- _reallyCompleteRequest: function(dismissed) {
- this._currentDialog.close();
- this._currentDialog.destroySession();
- this._currentDialog = null;
- this._isCompleting = false;
-
- this._native.complete(dismissed)
- },
-
- _completeRequest: function(keepVisible, wasDismissed) {
- if (this._isCompleting)
- return;
-
- this._isCompleting = true;
-
- if (keepVisible) {
- // Give the user 2 seconds to read 'Authentication Failure' before
- // dismissing the dialog
- Mainloop.timeout_add(2000,
- Lang.bind(this,
- function() {
- this._reallyCompleteRequest(wasDismissed);
- return false;
- }));
- } else {
- this._reallyCompleteRequest(wasDismissed);
- }
- }
-});
-
-function init() {
- let agent = new AuthenticationAgent();
-}
hasRunDialog: false,
hasWorkspaces: false,
hasWindows: false,
- createSession: Main.createGDMSession,
createUnlockDialog: Main.createGDMLoginDialog,
+ components: [],
panel: {
left: [],
center: ['dateMenu'],
hasRunDialog: false,
hasWorkspaces: false,
hasWindows: false,
+ components: ['networkAgent', 'polkitAgent', 'telepathyClient'],
panel: {
left: ['userMenu'],
center: [],
allowKeybindingsWhenModal: false,
hasRunDialog: false,
hasWorkspaces: false,
- createSession: Main.createInitialSetupSession,
+ components: ['keyring'],
panel: {
left: [],
center: ['dateMenu'],
hasRunDialog: true,
hasWorkspaces: true,
hasWindows: true,
- createSession: Main.createUserSession,
createUnlockDialog: Main.createSessionUnlockDialog,
+ components: ['networkAgent', 'polkitAgent', 'telepathyClient',
+ 'keyring', 'recorder', 'autorunManager', 'automountManager'],
panel: {
left: ['activities', 'appMenu'],
center: ['dateMenu'],
let params = _modes[this.currentMode];
params = Params.parse(params, _modes[DEFAULT_MODE]);
- this._createSession = params.createSession;
- delete params.createSession;
this._createUnlockDialog = params.createUnlockDialog;
delete params.createUnlockDialog;
this.emit('updated');
},
- createSession: function() {
- if (this._createSession)
- this._createSession();
- },
-
createUnlockDialog: function() {
if (this._createUnlockDialog)
return this._createUnlockDialog.apply(this, arguments);
+++ /dev/null
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const Lang = imports.lang;
-const Mainloop = imports.mainloop;
-const Shell = imports.gi.Shell;
-const Signals = imports.signals;
-const St = imports.gi.St;
-const Tpl = imports.gi.TelepathyLogger;
-const Tp = imports.gi.TelepathyGLib;
-
-const History = imports.misc.history;
-const Params = imports.misc.params;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-
-
-// See Notification.appendMessage
-const SCROLLBACK_IMMEDIATE_TIME = 60; // 1 minute
-const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
-const SCROLLBACK_RECENT_LENGTH = 20;
-const SCROLLBACK_IDLE_LENGTH = 5;
-
-// See Source._displayPendingMessages
-const SCROLLBACK_HISTORY_LINES = 10;
-
-// See Notification._onEntryChanged
-const COMPOSING_STOP_TIMEOUT = 5;
-
-const NotificationDirection = {
- SENT: 'chat-sent',
- RECEIVED: 'chat-received'
-};
-
-function makeMessageFromTpMessage(tpMessage, direction) {
- let [text, flags] = tpMessage.to_text();
-
- let timestamp = tpMessage.get_sent_timestamp();
- if (timestamp == 0)
- timestamp = tpMessage.get_received_timestamp();
-
- return {
- messageType: tpMessage.get_message_type(),
- text: text,
- sender: tpMessage.sender.alias,
- timestamp: timestamp,
- direction: direction
- };
-}
-
-
-function makeMessageFromTplEvent(event) {
- let sent = event.get_sender().get_entity_type() == Tpl.EntityType.SELF;
- let direction = sent ? NotificationDirection.SENT : NotificationDirection.RECEIVED;
-
- return {
- messageType: event.get_message_type(),
- text: event.get_message(),
- sender: event.get_sender().get_alias(),
- timestamp: event.get_timestamp(),
- direction: direction
- };
-}
-
-const Client = new Lang.Class({
- Name: 'Client',
-
- _init : function() {
- // channel path -> ChatSource
- this._chatSources = {};
- this._chatState = Tp.ChannelChatState.ACTIVE;
-
- // account path -> AccountNotification
- this._accountNotifications = {};
-
- // Define features we want
- this._accountManager = Tp.AccountManager.dup();
- let factory = this._accountManager.get_factory();
- factory.add_account_features([Tp.Account.get_feature_quark_connection()]);
- factory.add_connection_features([Tp.Connection.get_feature_quark_contact_list()]);
- factory.add_channel_features([Tp.Channel.get_feature_quark_contacts()]);
- factory.add_contact_features([Tp.ContactFeature.ALIAS,
- Tp.ContactFeature.AVATAR_DATA,
- Tp.ContactFeature.PRESENCE,
- Tp.ContactFeature.SUBSCRIPTION_STATES]);
-
- // Set up a SimpleObserver, which will call _observeChannels whenever a
- // channel matching its filters is detected.
- // The second argument, recover, means _observeChannels will be run
- // for any existing channel as well.
- this._tpClient = new Shell.TpClient({ 'account-manager': this._accountManager,
- 'name': 'GnomeShell',
- 'uniquify-name': true })
- this._tpClient.set_observe_channels_func(
- Lang.bind(this, this._observeChannels));
- this._tpClient.set_approve_channels_func(
- Lang.bind(this, this._approveChannels));
- this._tpClient.set_handle_channels_func(
- Lang.bind(this, this._handleChannels));
-
- // Workaround for gjs not supporting GPtrArray in signals.
- // See BGO bug #653941 for context.
- this._tpClient.set_contact_list_changed_func(
- Lang.bind(this, this._contactListChanged));
-
- // Allow other clients (such as Empathy) to pre-empt our channels if
- // needed
- this._tpClient.set_delegated_channels_callback(
- Lang.bind(this, this._delegatedChannelsCb));
-
- try {
- this._tpClient.register();
- } catch (e) {
- throw new Error('Couldn\'t register Telepathy client. Error: \n' + e);
- }
-
- // Watch subscription requests and connection errors
- this._subscriptionSource = null;
- this._accountSource = null;
-
- this._accountManager.connect('account-validity-changed',
- Lang.bind(this, this._accountValidityChanged));
-
- this._accountManager.prepare_async(null, Lang.bind(this, this._accountManagerPrepared));
- },
-
- _observeChannels: function(observer, account, conn, channels,
- dispatchOp, requests, context) {
- let len = channels.length;
- for (let i = 0; i < len; i++) {
- let channel = channels[i];
- let [targetHandle, targetHandleType] = channel.get_handle();
-
- if (channel.get_invalidated())
- continue;
-
- /* Only observe contact text channels */
- if ((!(channel instanceof Tp.TextChannel)) ||
- targetHandleType != Tp.HandleType.CONTACT)
- continue;
-
- this._createChatSource(account, conn, channel, channel.get_target_contact());
- }
-
- context.accept();
- },
-
- _createChatSource: function(account, conn, channel, contact) {
- if (this._chatSources[channel.get_object_path()])
- return;
-
- let source = new ChatSource(account, conn, channel, contact, this._tpClient);
-
- this._chatSources[channel.get_object_path()] = source;
- source.connect('destroy', Lang.bind(this,
- function() {
- if (this._tpClient.is_handling_channel(channel)) {
- // The chat box has been destroyed so it can't
- // handle the channel any more.
- channel.close_async(function(src, result) {
- channel.close_finish(result);
- });
- }
-
- delete this._chatSources[channel.get_object_path()];
- }));
- },
-
- _handleChannels: function(handler, account, conn, channels,
- requests, user_action_time, context) {
- this._handlingChannels(account, conn, channels, true);
- context.accept();
- },
-
- _handlingChannels: function(account, conn, channels, notify) {
- let len = channels.length;
- for (let i = 0; i < len; i++) {
- let channel = channels[i];
-
- // We can only handle text channel, so close any other channel
- if (!(channel instanceof Tp.TextChannel)) {
- channel.close_async(null);
- continue;
- }
-
- if (channel.get_invalidated())
- continue;
-
- // 'notify' will be true when coming from an actual HandleChannels
- // call, and not when from a successful Claim call. The point is
- // we don't want to notify for a channel we just claimed which
- // has no new messages (for example, a new channel which only has
- // a delivery notification). We rely on _displayPendingMessages()
- // and _messageReceived() to notify for new messages.
-
- // But we should still notify from HandleChannels because the
- // Telepathy spec states that handlers must foreground channels
- // in HandleChannels calls which are already being handled.
-
- if (notify && this._tpClient.is_handling_channel(channel)) {
- // We are already handling the channel, display the source
- let source = this._chatSources[channel.get_object_path()];
- if (source)
- source.notify();
- }
- }
- },
-
- _displayRoomInvitation: function(conn, channel, dispatchOp, context) {
- // We can only approve the rooms if we have been invited to it
- let selfContact = channel.group_get_self_contact();
- if (selfContact == null) {
- context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
- message: 'Not invited to the room' }));
- return;
- }
-
- let [invited, inviter, reason, msg] = channel.group_get_local_pending_contact_info(selfContact);
- if (!invited) {
- context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
- message: 'Not invited to the room' }));
- return;
- }
-
- // FIXME: We don't have a 'chat room' icon (bgo #653737) use
- // system-users for now as Empathy does.
- let source = new ApproverSource(dispatchOp, _("Invitation"),
- Gio.icon_new_for_string('system-users'));
- Main.messageTray.add(source);
-
- let notif = new RoomInviteNotification(source, dispatchOp, channel, inviter);
- source.notify(notif);
- context.accept();
- },
-
- _approveChannels: function(approver, account, conn, channels,
- dispatchOp, context) {
- let channel = channels[0];
- let chanType = channel.get_channel_type();
-
- if (channel.get_invalidated()) {
- context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
- message: 'Channel is invalidated' }));
- return;
- }
-
- if (chanType == Tp.IFACE_CHANNEL_TYPE_TEXT)
- this._approveTextChannel(account, conn, channel, dispatchOp, context);
- else if (chanType == Tp.IFACE_CHANNEL_TYPE_CALL)
- this._approveCall(account, conn, channel, dispatchOp, context);
- else if (chanType == Tp.IFACE_CHANNEL_TYPE_FILE_TRANSFER)
- this._approveFileTransfer(account, conn, channel, dispatchOp, context);
- else
- context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
- message: 'Unsupported channel type' }));
- },
-
- _approveTextChannel: function(account, conn, channel, dispatchOp, context) {
- let [targetHandle, targetHandleType] = channel.get_handle();
-
- if (targetHandleType == Tp.HandleType.CONTACT) {
- // Approve private text channels right away as we are going to handle it
- dispatchOp.claim_with_async(this._tpClient,
- Lang.bind(this, function(dispatchOp, result) {
- try {
- dispatchOp.claim_with_finish(result);
- this._handlingChannels(account, conn, [channel], false);
- } catch (err) {
- throw new Error('Failed to Claim channel: ' + err);
- }}));
-
- context.accept();
- } else {
- this._displayRoomInvitation(conn, channel, dispatchOp, context);
- }
- },
-
- _approveCall: function(account, conn, channel, dispatchOp, context) {
- let isVideo = false;
-
- let props = channel.borrow_immutable_properties();
-
- if (props[Tp.PROP_CHANNEL_TYPE_CALL_INITIAL_VIDEO])
- isVideo = true;
-
- // We got the TpContact
- let source = new ApproverSource(dispatchOp, _("Call"), isVideo ?
- Gio.icon_new_for_string('camera-web') :
- Gio.icon_new_for_string('audio-input-microphone'));
- Main.messageTray.add(source);
-
- let notif = new AudioVideoNotification(source, dispatchOp, channel,
- channel.get_target_contact(), isVideo);
- source.notify(notif);
- context.accept();
- },
-
- _approveFileTransfer: function(account, conn, channel, dispatchOp, context) {
- // Use the icon of the file being transferred
- let gicon = Gio.content_type_get_icon(channel.get_mime_type());
-
- // We got the TpContact
- let source = new ApproverSource(dispatchOp, _("File Transfer"), gicon);
- Main.messageTray.add(source);
-
- let notif = new FileTransferNotification(source, dispatchOp, channel,
- channel.get_target_contact());
- source.notify(notif);
- context.accept();
- },
-
- _delegatedChannelsCb: function(client, channels) {
- // Nothing to do as we don't make a distinction between observed and
- // handled channels.
- },
-
- _accountManagerPrepared: function(am, result) {
- am.prepare_finish(result);
-
- let accounts = am.get_valid_accounts();
- for (let i = 0; i < accounts.length; i++) {
- this._accountValidityChanged(am, accounts[i], true);
- }
- },
-
- _accountValidityChanged: function(am, account, valid) {
- if (!valid)
- return;
-
- // It would be better to connect to "status-changed" but we cannot.
- // See discussion in https://bugzilla.gnome.org/show_bug.cgi?id=654159
- account.connect("notify::connection-status",
- Lang.bind(this, this._accountConnectionStatusNotifyCb));
-
- account.connect('notify::connection',
- Lang.bind(this, this._connectionChanged));
- this._connectionChanged(account);
- },
-
- _connectionChanged: function(account) {
- let conn = account.get_connection();
- if (conn == null)
- return;
-
- this._tpClient.grab_contact_list_changed(conn);
- if (conn.get_contact_list_state() == Tp.ContactListState.SUCCESS) {
- this._contactListChanged(conn, conn.dup_contact_list(), []);
- }
- },
-
- _contactListChanged: function(conn, added, removed) {
- for (let i = 0; i < added.length; i++) {
- let contact = added[i];
-
- contact.connect('subscription-states-changed',
- Lang.bind(this, this._subscriptionStateChanged));
- this._subscriptionStateChanged(contact);
- }
- },
-
- _subscriptionStateChanged: function(contact) {
- if (contact.get_publish_state() != Tp.SubscriptionState.ASK)
- return;
-
- /* Implicitly accept publish requests if contact is already subscribed */
- if (contact.get_subscribe_state() == Tp.SubscriptionState.YES ||
- contact.get_subscribe_state() == Tp.SubscriptionState.ASK) {
-
- contact.authorize_publication_async(function(src, result) {
- src.authorize_publication_finish(result)});
-
- return;
- }
-
- /* Display notification to ask user to accept/reject request */
- let source = this._ensureSubscriptionSource();
-
- let notif = new SubscriptionRequestNotification(source, contact);
- source.notify(notif);
- },
-
- _ensureSubscriptionSource: function() {
- if (this._subscriptionSource == null) {
- this._subscriptionSource = new MessageTray.Source(_("Subscription request"),
- 'gtk-dialog-question');
- Main.messageTray.add(this._subscriptionSource);
- this._subscriptionSource.connect('destroy', Lang.bind(this, function () {
- this._subscriptionSource = null;
- }));
- }
-
- return this._subscriptionSource;
- },
-
- _accountConnectionStatusNotifyCb: function(account) {
- let connectionError = account.connection_error;
-
- if (account.connection_status != Tp.ConnectionStatus.DISCONNECTED ||
- connectionError == Tp.error_get_dbus_name(Tp.Error.CANCELLED)) {
- return;
- }
-
- let notif = this._accountNotifications[account.get_object_path()];
- if (notif)
- return;
-
- /* Display notification that account failed to connect */
- let source = this._ensureAccountSource();
-
- notif = new AccountNotification(source, account, connectionError);
- this._accountNotifications[account.get_object_path()] = notif;
- notif.connect('destroy', Lang.bind(this, function() {
- delete this._accountNotifications[account.get_object_path()];
- }));
- source.notify(notif);
- },
-
- _ensureAccountSource: function() {
- if (this._accountSource == null) {
- this._accountSource = new MessageTray.Source(_("Connection error"),
- 'gtk-dialog-error');
- Main.messageTray.add(this._accountSource);
- this._accountSource.connect('destroy', Lang.bind(this, function () {
- this._accountSource = null;
- }));
- }
-
- return this._accountSource;
- }
-});
-
-const ChatSource = new Lang.Class({
- Name: 'ChatSource',
- Extends: MessageTray.Source,
-
- _init: function(account, conn, channel, contact, client) {
- this._account = account;
- this._contact = contact;
- this._client = client;
-
- this.parent(contact.get_alias());
-
- this.isChat = true;
- this._pendingMessages = [];
-
- this._conn = conn;
- this._channel = channel;
- this._closedId = this._channel.connect('invalidated', Lang.bind(this, this._channelClosed));
-
- this._notification = new ChatNotification(this);
- this._notification.setUrgency(MessageTray.Urgency.HIGH);
- this._notifyTimeoutId = 0;
-
- // We ack messages when the user expands the new notification or views the summary
- // notification, in which case the notification is also expanded.
- this._notification.connect('expanded', Lang.bind(this, this._ackMessages));
-
- this._presence = contact.get_presence_type();
-
- this._sentId = this._channel.connect('message-sent', Lang.bind(this, this._messageSent));
- this._receivedId = this._channel.connect('message-received', Lang.bind(this, this._messageReceived));
- this._pendingId = this._channel.connect('pending-message-removed', Lang.bind(this, this._pendingRemoved));
-
- this._notifyAliasId = this._contact.connect('notify::alias', Lang.bind(this, this._updateAlias));
- this._notifyAvatarId = this._contact.connect('notify::avatar-file', Lang.bind(this, this._updateAvatarIcon));
- this._presenceChangedId = this._contact.connect('presence-changed', Lang.bind(this, this._presenceChanged));
-
- // Add ourselves as a source.
- Main.messageTray.add(this);
- this.pushNotification(this._notification);
-
- this._getLogMessages();
- },
-
- _updateAlias: function() {
- let oldAlias = this.title;
- let newAlias = this._contact.get_alias();
-
- if (oldAlias == newAlias)
- return;
-
- this.setTitle(newAlias);
- this._notification.appendAliasChange(oldAlias, newAlias);
- },
-
- createIcon: function(size) {
- this._iconBox = new St.Bin({ style_class: 'avatar-box' });
- this._iconBox._size = size;
- let textureCache = St.TextureCache.get_default();
- let file = this._contact.get_avatar_file();
-
- if (file) {
- let uri = file.get_uri();
- this._iconBox.child = textureCache.load_uri_async(uri, this._iconBox._size, this._iconBox._size);
- } else {
- this._iconBox.child = new St.Icon({ icon_name: 'avatar-default',
- icon_size: this._iconBox._size });
- }
-
- return this._iconBox;
- },
-
- createSecondaryIcon: function() {
- let iconBox = new St.Bin();
- iconBox.child = new St.Icon({ style_class: 'secondary-icon' });
- let presenceType = this._contact.get_presence_type();
-
- switch (presenceType) {
- case Tp.ConnectionPresenceType.AVAILABLE:
- iconBox.child.icon_name = 'user-available';
- break;
- case Tp.ConnectionPresenceType.BUSY:
- iconBox.child.icon_name = 'user-busy';
- break;
- case Tp.ConnectionPresenceType.OFFLINE:
- iconBox.child.icon_name = 'user-offline';
- break;
- case Tp.ConnectionPresenceType.HIDDEN:
- iconBox.child.icon_name = 'user-invisible';
- break;
- case Tp.ConnectionPresenceType.AWAY:
- iconBox.child.icon_name = 'user-away';
- break;
- case Tp.ConnectionPresenceType.EXTENDED_AWAY:
- iconBox.child.icon_name = 'user-idle';
- break;
- default:
- iconBox.child.icon_name = 'user-offline';
- }
- return iconBox;
- },
-
- _updateAvatarIcon: function() {
- this.iconUpdated();
- this._notification.update(this._notification.title, null, { customContent: true });
- },
-
- open: function(notification) {
- if (this._client.is_handling_channel(this._channel)) {
- // We are handling the channel, try to pass it to Empathy
- this._client.delegate_channels_async([this._channel], global.get_current_time(), '', null);
- }
- else {
- // We are not the handler, just ask to present the channel
- let dbus = Tp.DBusDaemon.dup();
- let cd = Tp.ChannelDispatcher.new(dbus);
-
- cd.present_channel_async(this._channel, global.get_current_time(), null);
- }
- },
-
- _getLogMessages: function() {
- let logManager = Tpl.LogManager.dup_singleton();
- let entity = Tpl.Entity.new_from_tp_contact(this._contact, Tpl.EntityType.CONTACT);
-
- logManager.get_filtered_events_async(this._account, entity,
- Tpl.EventTypeMask.TEXT, SCROLLBACK_HISTORY_LINES,
- null, Lang.bind(this, this._displayPendingMessages));
- },
-
- _displayPendingMessages: function(logManager, result) {
- let [success, events] = logManager.get_filtered_events_finish(result);
-
- let logMessages = events.map(makeMessageFromTplEvent);
-
- let pendingTpMessages = this._channel.get_pending_messages();
- let pendingMessages = [];
-
- for (let i = 0; i < pendingTpMessages.length; i++) {
- let message = pendingTpMessages[i];
-
- if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
- continue;
-
- pendingMessages.push(makeMessageFromTpMessage(message, NotificationDirection.RECEIVED));
-
- this._pendingMessages.push(message);
- }
-
- this.countUpdated();
-
- let showTimestamp = false;
-
- for (let i = 0; i < logMessages.length; i++) {
- let logMessage = logMessages[i];
- let isPending = false;
-
- // Skip any log messages that are also in pendingMessages
- for (let j = 0; j < pendingMessages.length; j++) {
- let pending = pendingMessages[j];
- if (logMessage.timestamp == pending.timestamp && logMessage.text == pending.text) {
- isPending = true;
- break;
- }
- }
-
- if (!isPending) {
- showTimestamp = true;
- this._notification.appendMessage(logMessage, true, ['chat-log-message']);
- }
- }
-
- if (showTimestamp)
- this._notification.appendTimestamp();
-
- for (let i = 0; i < pendingMessages.length; i++)
- this._notification.appendMessage(pendingMessages[i], true);
-
- if (pendingMessages.length > 0)
- this.notify();
- },
-
- _channelClosed: function() {
- this._channel.disconnect(this._closedId);
- this._channel.disconnect(this._receivedId);
- this._channel.disconnect(this._pendingId);
- this._channel.disconnect(this._sentId);
-
- this._contact.disconnect(this._notifyAliasId);
- this._contact.disconnect(this._notifyAvatarId);
- this._contact.disconnect(this._presenceChangedId);
-
- this.destroy();
- },
-
- /* All messages are new messages for Telepathy sources */
- get count() {
- return this._pendingMessages.length;
- },
-
- get unseenCount() {
- return this.count;
- },
-
- get countVisible() {
- return this.count > 0;
- },
-
- _messageReceived: function(channel, message) {
- if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
- return;
-
- this._pendingMessages.push(message);
- this.countUpdated();
-
- message = makeMessageFromTpMessage(message, NotificationDirection.RECEIVED);
- this._notification.appendMessage(message);
-
- // Wait a bit before notifying for the received message, a handler
- // could ack it in the meantime.
- if (this._notifyTimeoutId != 0)
- Mainloop.source_remove(this._notifyTimeoutId);
- this._notifyTimeoutId = Mainloop.timeout_add(500,
- Lang.bind(this, this._notifyTimeout));
- },
-
- _notifyTimeout: function() {
- if (this._pendingMessages.length != 0)
- this.notify();
-
- this._notifyTimeoutId = 0;
-
- return false;
- },
-
- // This is called for both messages we send from
- // our client and other clients as well.
- _messageSent: function(channel, message, flags, token) {
- message = makeMessageFromTpMessage(message, NotificationDirection.SENT);
- this._notification.appendMessage(message);
- },
-
- notify: function() {
- this.parent(this._notification);
- },
-
- respond: function(text) {
- let type;
- if (text.slice(0, 4) == '/me ') {
- type = Tp.ChannelTextMessageType.ACTION;
- text = text.slice(4);
- } else {
- type = Tp.ChannelTextMessageType.NORMAL;
- }
-
- let msg = Tp.ClientMessage.new_text(type, text);
- this._channel.send_message_async(msg, 0, Lang.bind(this, function (src, result) {
- this._channel.send_message_finish(result);
- }));
- },
-
- setChatState: function(state) {
- // We don't want to send COMPOSING every time a letter is typed into
- // the entry. We send the state only when it changes. Telepathy/Empathy
- // might change it behind our back if the user is using both
- // gnome-shell's entry and the Empathy conversation window. We could
- // keep track of it with the ChatStateChanged signal but it is good
- // enough right now.
- if (state != this._chatState) {
- this._chatState = state;
- this._channel.set_chat_state_async(state, null);
- }
- },
-
- _presenceChanged: function (contact, presence, status, message) {
- let msg, title;
-
- title = GLib.markup_escape_text(this.title, -1);
-
- this._notification.update(this._notification.title, null, { customContent: true, secondaryIcon: this.createSecondaryIcon() });
-
- if (message)
- msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
- },
-
- _pendingRemoved: function(channel, message) {
- let idx = this._pendingMessages.indexOf(message);
-
- if (idx >= 0) {
- this._pendingMessages.splice(idx, 1);
- this.countUpdated();
- }
- else
- throw new Error('Message not in our pending list: ' + message);
- },
-
- _ackMessages: function() {
- // Don't clear our messages here, tp-glib will send a
- // 'pending-message-removed' for each one.
- this._channel.ack_all_pending_messages_async(Lang.bind(this, function(src, result) {
- this._channel.ack_all_pending_messages_finish(result);}));
- }
-});
-
-const ChatNotification = new Lang.Class({
- Name: 'ChatNotification',
- Extends: MessageTray.Notification,
-
- _init: function(source) {
- this.parent(source, source.title, null, { customContent: true, secondaryIcon: source.createSecondaryIcon() });
- this.setResident(true);
-
- this._responseEntry = new St.Entry({ style_class: 'chat-response',
- can_focus: true });
- this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated));
- this._responseEntry.clutter_text.connect('text-changed', Lang.bind(this, this._onEntryChanged));
- this.setActionArea(this._responseEntry);
-
- this._oldMaxScrollAdjustment = 0;
- this._createScrollArea();
- this._lastGroup = null;
- this._lastGroupActor = null;
-
- this._scrollArea.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
- let currentValue = adjustment.value + adjustment.page_size;
- if (currentValue == this._oldMaxScrollAdjustment)
- this.scrollTo(St.Side.BOTTOM);
- this._oldMaxScrollAdjustment = adjustment.upper;
- }));
-
- this._inputHistory = new History.HistoryManager({ entry: this._responseEntry.clutter_text });
-
- this._history = [];
- this._timestampTimeoutId = 0;
- this._composingTimeoutId = 0;
- },
-
- /**
- * appendMessage:
- * @message: An object with the properties:
- * text: the body of the message,
- * messageType: a #Tp.ChannelTextMessageType,
- * sender: the name of the sender,
- * timestamp: the time the message was sent
- * direction: a #NotificationDirection
- *
- * @noTimestamp: Whether to add a timestamp. If %true, no timestamp
- * will be added, regardless of the difference since the
- * last timestamp
- */
- appendMessage: function(message, noTimestamp) {
- let messageBody = GLib.markup_escape_text(message.text, -1);
- let styles = [message.direction];
-
- if (message.messageType == Tp.ChannelTextMessageType.ACTION) {
- let senderAlias = GLib.markup_escape_text(message.sender, -1);
- messageBody = '<i>%s</i> %s'.format(senderAlias, messageBody);
- styles.push('chat-action');
- }
-
- if (message.direction == NotificationDirection.RECEIVED) {
- this.update(this.source.title, messageBody, { customContent: true,
- bannerMarkup: true });
- }
-
- let group = (message.direction == NotificationDirection.RECEIVED ?
- 'received' : 'sent');
-
- this._append({ body: messageBody,
- group: group,
- styles: styles,
- timestamp: message.timestamp,
- noTimestamp: noTimestamp });
- },
-
- _filterMessages: function() {
- if (this._history.length < 1)
- return;
-
- let lastMessageTime = this._history[0].time;
- let currentTime = (Date.now() / 1000);
-
- // Keep the scrollback from growing too long. If the most
- // recent message (before the one we just added) is within
- // SCROLLBACK_RECENT_TIME, we will keep
- // SCROLLBACK_RECENT_LENGTH previous messages. Otherwise
- // we'll keep SCROLLBACK_IDLE_LENGTH messages.
-
- let maxLength = (lastMessageTime < currentTime - SCROLLBACK_RECENT_TIME) ?
- SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
-
- let filteredHistory = this._history.filter(function(item) { return item.realMessage });
- if (filteredHistory.length > maxLength) {
- let lastMessageToKeep = filteredHistory[maxLength];
- let expired = this._history.splice(this._history.indexOf(lastMessageToKeep));
- for (let i = 0; i < expired.length; i++)
- expired[i].actor.destroy();
- }
-
- let groups = this._contentArea.get_children();
- for (let i = 0; i < groups.length; i++) {
- let group = groups[i];
- if (group.get_n_children() == 0)
- group.destroy();
- }
- },
-
- /**
- * _append:
- * @props: An object with the properties:
- * body: The text of the message.
- * group: The group of the message, one of:
- * 'received', 'sent', 'meta'.
- * styles: Style class names for the message to have.
- * timestamp: The timestamp of the message.
- * noTimestamp: suppress timestamp signal?
- * childProps: props to add the actor with.
- */
- _append: function(props) {
- let currentTime = (Date.now() / 1000);
- props = Params.parse(props, { body: null,
- group: null,
- styles: [],
- timestamp: currentTime,
- noTimestamp: false,
- childProps: null });
-
- // Reset the old message timeout
- if (this._timestampTimeoutId)
- Mainloop.source_remove(this._timestampTimeoutId);
-
- let highlighter = new MessageTray.URLHighlighter(props.body,
- true, // line wrap?
- true); // allow markup?
-
- let body = highlighter.actor;
-
- let styles = props.styles;
- for (let i = 0; i < styles.length; i++)
- body.add_style_class_name(styles[i]);
-
- let group = props.group;
- if (group != this._lastGroup) {
- let style = 'chat-group-' + group;
- this._lastGroup = group;
- this._lastGroupActor = new St.BoxLayout({ style_class: style,
- vertical: true });
- this.addActor(this._lastGroupActor);
- }
-
- this._lastGroupActor.add(body, props.childProps);
-
- this.updated();
-
- let timestamp = props.timestamp;
- this._history.unshift({ actor: body, time: timestamp,
- realMessage: group != 'meta' });
-
- if (!props.noTimestamp) {
- if (timestamp < currentTime - SCROLLBACK_IMMEDIATE_TIME)
- this.appendTimestamp();
- else
- // Schedule a new timestamp in SCROLLBACK_IMMEDIATE_TIME
- // from the timestamp of the message.
- this._timestampTimeoutId = Mainloop.timeout_add_seconds(
- SCROLLBACK_IMMEDIATE_TIME - (currentTime - timestamp),
- Lang.bind(this, this.appendTimestamp));
- }
-
- this._filterMessages();
- },
-
- _formatTimestamp: function(date) {
- let now = new Date();
-
- var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
-
- let format;
-
- // Show a week day and time if date is in the last week
- if (daysAgo < 1 || (daysAgo < 7 && now.getDay() != date.getDay())) {
- /* Translators: this is a time format string followed by a date.
- If applicable, replace %X with a strftime format valid for your
- locale, without seconds. */
- // xgettext:no-c-format
- format = _("Sent at <b>%X</b> on <b>%A</b>");
-
- } else if (date.getYear() == now.getYear()) {
- /* Translators: this is a time format in the style of "Wednesday, May 25",
- shown when you get a chat message in the same year. */
- // xgettext:no-c-format
- format = _("Sent on <b>%A</b>, <b>%B %d</b>");
- } else {
- /* Translators: this is a time format in the style of "Wednesday, May 25, 2012",
- shown when you get a chat message in a different year. */
- // xgettext:no-c-format
- format = _("Sent on <b>%A</b>, <b>%B %d</b>, %Y");
- }
-
- return date.toLocaleFormat(format);
- },
-
- appendTimestamp: function() {
- let lastMessageTime = this._history[0].time;
- let lastMessageDate = new Date(lastMessageTime * 1000);
-
- let timeLabel = this._append({ body: this._formatTimestamp(lastMessageDate),
- group: 'meta',
- styles: ['chat-meta-message'],
- childProps: { expand: true, x_fill: false,
- x_align: St.Align.END },
- noTimestamp: true,
- timestamp: lastMessageTime });
-
- this._filterMessages();
-
- return false;
- },
-
- appendAliasChange: function(oldAlias, newAlias) {
- oldAlias = GLib.markup_escape_text(oldAlias, -1);
- newAlias = GLib.markup_escape_text(newAlias, -1);
-
- /* Translators: this is the other person changing their old IM name to their new
- IM name. */
- let message = '<i>' + _("%s is now known as %s").format(oldAlias, newAlias) + '</i>';
-
- let label = this._append({ body: message,
- group: 'meta',
- styles: ['chat-meta-message'] });
-
- this.update(newAlias, null, { customContent: true });
-
- this._filterMessages();
- },
-
- _onEntryActivated: function() {
- let text = this._responseEntry.get_text();
- if (text == '')
- return;
-
- this._inputHistory.addItem(text);
-
- // Telepathy sends out the Sent signal for us.
- // see Source._messageSent
- this._responseEntry.set_text('');
- this.source.respond(text);
- },
-
- _composingStopTimeout: function() {
- this._composingTimeoutId = 0;
-
- this.source.setChatState(Tp.ChannelChatState.PAUSED);
-
- return false;
- },
-
- _onEntryChanged: function() {
- let text = this._responseEntry.get_text();
-
- // If we're typing, we want to send COMPOSING.
- // If we empty the entry, we want to send ACTIVE.
- // If we've stopped typing for COMPOSING_STOP_TIMEOUT
- // seconds, we want to send PAUSED.
-
- // Remove composing timeout.
- if (this._composingTimeoutId > 0) {
- Mainloop.source_remove(this._composingTimeoutId);
- this._composingTimeoutId = 0;
- }
-
- if (text != '') {
- this.source.setChatState(Tp.ChannelChatState.COMPOSING);
-
- this._composingTimeoutId = Mainloop.timeout_add_seconds(
- COMPOSING_STOP_TIMEOUT,
- Lang.bind(this, this._composingStopTimeout));
- } else {
- this.source.setChatState(Tp.ChannelChatState.ACTIVE);
- }
- }
-});
-
-const ApproverSource = new Lang.Class({
- Name: 'ApproverSource',
- Extends: MessageTray.Source,
-
- _init: function(dispatchOp, text, gicon) {
- this._gicon = gicon;
-
- this.parent(text);
-
- this._dispatchOp = dispatchOp;
-
- // Destroy the source if the channel dispatch operation is invalidated
- // as we can't approve any more.
- this._invalidId = dispatchOp.connect('invalidated',
- Lang.bind(this, function(domain, code, msg) {
- this.destroy();
- }));
- },
-
- destroy: function() {
- if (this._invalidId != 0) {
- this._dispatchOp.disconnect(this._invalidId);
- this._invalidId = 0;
- }
-
- this.parent();
- },
-
- createIcon: function(size) {
- return new St.Icon({ gicon: this._gicon,
- icon_size: size });
- }
-});
-
-const RoomInviteNotification = new Lang.Class({
- Name: 'RoomInviteNotification',
- Extends: MessageTray.Notification,
-
- _init: function(source, dispatchOp, channel, inviter) {
- this.parent(source,
- /* translators: argument is a room name like
- * room@jabber.org for example. */
- _("Invitation to %s").format(channel.get_identifier()),
- null,
- { customContent: true });
- this.setResident(true);
-
- /* translators: first argument is the name of a contact and the second
- * one the name of a room. "Alice is inviting you to join room@jabber.org
- * for example. */
- this.addBody(_("%s is inviting you to join %s").format(inviter.get_alias(), channel.get_identifier()));
-
- this.addButton('decline', _("Decline"));
- this.addButton('accept', _("Accept"));
-
- this.connect('action-invoked', Lang.bind(this, function(self, action) {
- switch (action) {
- case 'decline':
- dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
- '', function(src, result) {
- src.leave_channels_finish(result)});
- break;
- case 'accept':
- dispatchOp.handle_with_time_async('', global.get_current_time(),
- function(src, result) {
- src.handle_with_time_finish(result)});
- break;
- }
- this.destroy();
- }));
- }
-});
-
-// Audio Video
-const AudioVideoNotification = new Lang.Class({
- Name: 'AudioVideoNotification',
- Extends: MessageTray.Notification,
-
- _init: function(source, dispatchOp, channel, contact, isVideo) {
- let title = '';
-
- if (isVideo)
- /* translators: argument is a contact name like Alice for example. */
- title = _("Video call from %s").format(contact.get_alias());
- else
- /* translators: argument is a contact name like Alice for example. */
- title = _("Call from %s").format(contact.get_alias());
-
- this.parent(source, title, null, { customContent: true });
- this.setResident(true);
-
- this.addButton('reject', _("Reject"));
- /* translators: this is a button label (verb), not a noun */
- this.addButton('answer', _("Answer"));
-
- this.connect('action-invoked', Lang.bind(this, function(self, action) {
- switch (action) {
- case 'reject':
- dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
- '', function(src, result) {
- src.leave_channels_finish(result)});
- break;
- case 'answer':
- dispatchOp.handle_with_time_async('', global.get_current_time(),
- function(src, result) {
- src.handle_with_time_finish(result)});
- break;
- }
- this.destroy();
- }));
- }
-});
-
-// File Transfer
-const FileTransferNotification = new Lang.Class({
- Name: 'FileTransferNotification',
- Extends: MessageTray.Notification,
-
- _init: function(source, dispatchOp, channel, contact) {
- this.parent(source,
- /* To translators: The first parameter is
- * the contact's alias and the second one is the
- * file name. The string will be something
- * like: "Alice is sending you test.ogg"
- */
- _("%s is sending you %s").format(contact.get_alias(),
- channel.get_filename()),
- null,
- { customContent: true });
- this.setResident(true);
-
- this.addButton('decline', _("Decline"));
- this.addButton('accept', _("Accept"));
-
- this.connect('action-invoked', Lang.bind(this, function(self, action) {
- switch (action) {
- case 'decline':
- dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
- '', function(src, result) {
- src.leave_channels_finish(result)});
- break;
- case 'accept':
- dispatchOp.handle_with_time_async('', global.get_current_time(),
- function(src, result) {
- src.handle_with_time_finish(result)});
- break;
- }
- this.destroy();
- }));
- }
-});
-
-// Subscription request
-const SubscriptionRequestNotification = new Lang.Class({
- Name: 'SubscriptionRequestNotification',
- Extends: MessageTray.Notification,
-
- _init: function(source, contact) {
- this.parent(source,
- /* To translators: The parameter is the contact's alias */
- _("%s would like permission to see when you are online").format(contact.get_alias()),
- null, { customContent: true });
-
- this._contact = contact;
- this._connection = contact.get_connection();
-
- let layout = new St.BoxLayout({ vertical: false });
-
- // Display avatar
- let iconBox = new St.Bin({ style_class: 'avatar-box' });
- iconBox._size = 48;
-
- let textureCache = St.TextureCache.get_default();
- let file = contact.get_avatar_file();
-
- if (file) {
- let uri = file.get_uri();
- iconBox.child = textureCache.load_uri_async(uri, iconBox._size, iconBox._size);
- }
- else {
- iconBox.child = new St.Icon({ icon_name: 'avatar-default',
- icon_size: iconBox._size });
- }
-
- layout.add(iconBox);
-
- // subscription request message
- let label = new St.Label({ style_class: 'subscription-message',
- text: contact.get_publish_request() });
-
- layout.add(label);
-
- this.addActor(layout);
-
- this.addButton('decline', _("Decline"));
- this.addButton('accept', _("Accept"));
-
- this.connect('action-invoked', Lang.bind(this, function(self, action) {
- switch (action) {
- case 'decline':
- contact.remove_async(function(src, result) {
- src.remove_finish(result)});
- break;
- case 'accept':
- // Authorize the contact and request to see his status as well
- contact.authorize_publication_async(function(src, result) {
- src.authorize_publication_finish(result)});
-
- contact.request_subscription_async('', function(src, result) {
- src.request_subscription_finish(result)});
- break;
- }
-
- // rely on _subscriptionStatesChangedCb to destroy the
- // notification
- }));
-
- this._changedId = contact.connect('subscription-states-changed',
- Lang.bind(this, this._subscriptionStatesChangedCb));
- this._invalidatedId = this._connection.connect('invalidated',
- Lang.bind(this, this.destroy));
- },
-
- destroy: function() {
- if (this._changedId != 0) {
- this._contact.disconnect(this._changedId);
- this._changedId = 0;
- }
-
- if (this._invalidatedId != 0) {
- this._connection.disconnect(this._invalidatedId);
- this._invalidatedId = 0;
- }
-
- this.parent();
- },
-
- _subscriptionStatesChangedCb: function(contact, subscribe, publish, msg) {
- // Destroy the notification if the subscription request has been
- // answered
- if (publish != Tp.SubscriptionState.ASK)
- this.destroy();
- }
-});
-
-// Messages from empathy/libempathy/empathy-utils.c
-// create_errors_to_message_hash()
-
-/* Translator note: these should be the same messages that are
- * used in Empathy, so just copy and paste from there. */
-let _connectionErrorMessages = {};
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.NETWORK_ERROR)]
- = _("Network error");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.AUTHENTICATION_FAILED)]
- = _("Authentication failed");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_ERROR)]
- = _("Encryption error");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_PROVIDED)]
- = _("Certificate not provided");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_UNTRUSTED)]
- = _("Certificate untrusted");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_EXPIRED)]
- = _("Certificate expired");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_ACTIVATED)]
- = _("Certificate not activated");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_HOSTNAME_MISMATCH)]
- = _("Certificate hostname mismatch");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_FINGERPRINT_MISMATCH)]
- = _("Certificate fingerprint mismatch");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_SELF_SIGNED)]
- = _("Certificate self-signed");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CANCELLED)]
- = _("Status is set to offline");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_NOT_AVAILABLE)]
- = _("Encryption is not available");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INVALID)]
- = _("Certificate is invalid");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REFUSED)]
- = _("Connection has been refused");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_FAILED)]
- = _("Connection can't be established");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_LOST)]
- = _("Connection has been lost");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ALREADY_CONNECTED)]
- = _("This account is already connected to the server");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REPLACED)]
- = _("Connection has been replaced by a new connection using the same resource");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.REGISTRATION_EXISTS)]
- = _("The account already exists on the server");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.SERVICE_BUSY)]
- = _("Server is currently too busy to handle the connection");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_REVOKED)]
- = _("Certificate has been revoked");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INSECURE)]
- = _("Certificate uses an insecure cipher algorithm or is cryptographically weak");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_LIMIT_EXCEEDED)]
- = _("The length of the server certificate, or the depth of the server certificate chain, exceed the limits imposed by the cryptography library");
-_connectionErrorMessages['org.freedesktop.DBus.Error.NoReply']
- = _("Internal error");
-
-const AccountNotification = new Lang.Class({
- Name: 'AccountNotification',
- Extends: MessageTray.Notification,
-
- _init: function(source, account, connectionError) {
- this.parent(source,
- /* translators: argument is the account name, like
- * name@jabber.org for example. */
- _("Connection to %s failed").format(account.get_display_name()),
- null, { customContent: true });
-
- this._label = new St.Label();
- this.addActor(this._label);
- this._updateMessage(connectionError);
-
- this._account = account;
-
- this.addButton('reconnect', _("Reconnect"));
- this.addButton('edit', _("Edit account"));
-
- this.connect('action-invoked', Lang.bind(this, function(self, action) {
- switch (action) {
- case 'reconnect':
- // If it fails again, a new notification should pop up with the
- // new error.
- account.reconnect_async(null);
- break;
- case 'edit':
- let cmd = '/usr/bin/empathy-accounts'
- + ' --select-account=%s'
- .format(account.get_path_suffix());
- let app_info = Gio.app_info_create_from_commandline(cmd, null, 0);
- app_info.launch([], global.create_app_launch_context());
- break;
- }
- this.destroy();
- }));
-
- this._enabledId = account.connect('notify::enabled',
- Lang.bind(this, function() {
- if (!account.is_enabled())
- this.destroy();
- }));
-
- this._invalidatedId = account.connect('invalidated',
- Lang.bind(this, this.destroy));
-
- this._connectionStatusId = account.connect('notify::connection-status',
- Lang.bind(this, function() {
- let status = account.connection_status;
- if (status == Tp.ConnectionStatus.CONNECTED) {
- this.destroy();
- } else if (status == Tp.ConnectionStatus.DISCONNECTED) {
- this._updateMessage(account.connection_error);
- }
- }));
- },
-
- _updateMessage: function(connectionError) {
- let message;
- if (connectionError in _connectionErrorMessages) {
- message = _connectionErrorMessages[connectionError];
- } else {
- message = _("Unknown reason");
- }
- this._label.set_text(message);
- },
-
- destroy: function() {
- if (this._enabledId != 0) {
- this._account.disconnect(this._enabledId);
- this._enabledId = 0;
- }
-
- if (this._invalidatedId != 0) {
- this._account.disconnect(this._invalidatedId);
- this._invalidatedId = 0;
- }
-
- if (this._connectionStatusId != 0) {
- this._account.disconnect(this._connectionStatusId);
- this._connectionStatusId = 0;
- }
-
- this.parent();
- }
-});
PolkitAgentListener parent_instance;
GList *scheduled_requests;
-
AuthRequest *current_request;
+
+ gpointer handle;
};
/* Signals */
GAsyncResult *res,
GError **error);
-static void
+void
shell_polkit_authentication_agent_init (ShellPolkitAuthenticationAgent *agent)
{
- gpointer handle;
- PolkitSubject *subject;
- GError *error;
+}
- subject = NULL;
+void
+shell_polkit_authentication_agent_register (ShellPolkitAuthenticationAgent *agent,
+ GError **error_out)
+{
+ GError *error = NULL;
+ PolkitSubject *subject;
- error = NULL;
subject = polkit_unix_session_new_for_process_sync (getpid (),
NULL, /* GCancellable* */
&error);
if (subject == NULL)
{
- if (error) /* polkit version 104 and older don't properly set error on failure */
- {
- g_warning ("Error getting session for the process we are in: %s (%s %d)",
- error->message,
- g_quark_to_string (error->domain),
- error->code);
- g_error_free (error);
- }
- else
- {
- g_warning ("Error getting session for the process we are in");
- }
+ if (error == NULL) /* polkit version 104 and older don't properly set error on failure */
+ error = g_error_new (POLKIT_ERROR, POLKIT_ERROR_FAILED,
+ "PolKit failed to properly get our session");
goto out;
}
- handle = polkit_agent_listener_register (POLKIT_AGENT_LISTENER (agent),
- POLKIT_AGENT_REGISTER_FLAGS_NONE,
- subject,
- NULL, /* use default object path */
- NULL, /* GCancellable */
- &error);
- if (handle == NULL)
- {
- g_warning ("Error registering polkit authentication agent: %s (%s %d)",
- error->message,
- g_quark_to_string (error->domain),
- error->code);
- g_error_free (error);
- goto out;
- }
-
- /* We don't need to register so skip saving handle */
+ agent->handle = polkit_agent_listener_register (POLKIT_AGENT_LISTENER (agent),
+ POLKIT_AGENT_REGISTER_FLAGS_NONE,
+ subject,
+ NULL, /* use default object path */
+ NULL, /* GCancellable */
+ &error);
out:
+ if (error != NULL)
+ g_propagate_error (error_out, error);
+
if (subject != NULL)
g_object_unref (subject);
}
static void
shell_polkit_authentication_agent_finalize (GObject *object)
{
- /* ShellPolkitAuthenticationAgent *agent = SHELL_POLKIT_AUTHENTICATION_AGENT (object); */
-
- /* Specifically left empty since the object stays alive forever - if code
- * is reused it would need to free outstanding requests etc.
- */
-
+ ShellPolkitAuthenticationAgent *agent = SHELL_POLKIT_AUTHENTICATION_AGENT (object);
+ shell_polkit_authentication_agent_unregister (agent);
G_OBJECT_CLASS (shell_polkit_authentication_agent_parent_class)->finalize (object);
}
g_idle_add (handle_cancelled_in_idle, request);
}
+static void
+auth_request_dismiss (AuthRequest *request)
+{
+ auth_request_complete (request, TRUE);
+}
+
+void
+shell_polkit_authentication_agent_unregister (ShellPolkitAuthenticationAgent *agent)
+{
+ if (agent->scheduled_requests != NULL)
+ {
+ g_list_foreach (agent->scheduled_requests, (GFunc)auth_request_dismiss, NULL);
+ agent->scheduled_requests = NULL;
+ }
+ if (agent->current_request != NULL)
+ auth_request_dismiss (agent->current_request);
+
+ polkit_agent_listener_unregister (agent->handle);
+ agent->handle = NULL;
+}
+
static void maybe_process_next_request (ShellPolkitAuthenticationAgent *agent);
static void
ShellPolkitAuthenticationAgent *shell_polkit_authentication_agent_new (void);
void shell_polkit_authentication_agent_complete (ShellPolkitAuthenticationAgent *agent,
gboolean dismissed);
+void shell_polkit_authentication_agent_register (ShellPolkitAuthenticationAgent *agent,
+ GError **error_out);
+void shell_polkit_authentication_agent_unregister (ShellPolkitAuthenticationAgent *agent);
G_END_DECLS